From 700f8d7802e017aa05221453a2fd97f66770f150 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Fri, 22 Aug 2025 11:15:54 -0700 Subject: [PATCH 01/82] fix links of h100_prefill_performance.png and h100_decode_performance.png Signed-off-by: Jason Zhou --- docs/architecture/pre_deployment_profiling.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture/pre_deployment_profiling.md b/docs/architecture/pre_deployment_profiling.md index e8d0fcf76a..e76fc985f5 100644 --- a/docs/architecture/pre_deployment_profiling.md +++ b/docs/architecture/pre_deployment_profiling.md @@ -23,8 +23,8 @@ The script will first detect the number of available GPUs on the current nodes ( After the profiling finishes, two plots will be generated in the `output-dir`. For example, here are the profiling results for `examples/llm/configs/disagg.yaml`: -![Prefill Performance](../images/h100_prefill_performance.png) -![Decode Performance](../images/h100_decode_performance.png) +![Prefill Performance](../../docs/images/h100_prefill_performance.png) +![Decode Performance](../../docs/images/h100_decode_performance.png) For the prefill performance, the script will plot the TTFT for different TP sizes and select the best TP size that meet the target TTFT SLA and delivers the best throughput per GPU. Based on how close the TTFT of the selected TP size is to the SLA, the script will also recommend the upper and lower bounds of the prefill queue size to be used in planner. From dc497226f13b69a20a795d4f070c462f0f2e869e Mon Sep 17 00:00:00 2001 From: J Wyman Date: Fri, 22 Aug 2025 13:33:41 -0400 Subject: [PATCH 02/82] feat: Remove Duplicate Multimodel Nixl Connect Example (#2622) Signed-off-by: Jason Zhou --- container/Dockerfile.vllm | 2 +- .../multimodal/components/encode_worker.py | 6 +- examples/multimodal/components/worker.py | 4 +- examples/multimodal/connect/README.md | 342 ---- examples/multimodal/connect/__init__.py | 1472 ----------------- examples/multimodal/utils/protocol.py | 9 +- 6 files changed, 11 insertions(+), 1824 deletions(-) delete mode 100644 examples/multimodal/connect/README.md delete mode 100644 examples/multimodal/connect/__init__.py diff --git a/container/Dockerfile.vllm b/container/Dockerfile.vllm index c8a4187540..d55fe262d7 100644 --- a/container/Dockerfile.vllm +++ b/container/Dockerfile.vllm @@ -291,7 +291,7 @@ RUN uv pip install maturin[patchelf] USER $USERNAME ENV HOME=/home/$USERNAME -ENV PYTHONPATH=$PYTHONPATH:$HOME/dynamo/components/planner/src:$PYTHONPATH +ENV PYTHONPATH=$PYTHONPATH:$HOME/dynamo/components/planner/src ENV CARGO_TARGET_DIR=$HOME/dynamo/.build/target WORKDIR $HOME diff --git a/examples/multimodal/components/encode_worker.py b/examples/multimodal/components/encode_worker.py index 7294df9408..5b743d1275 100644 --- a/examples/multimodal/components/encode_worker.py +++ b/examples/multimodal/components/encode_worker.py @@ -27,11 +27,11 @@ from vllm.engine.arg_utils import AsyncEngineArgs from vllm.utils import FlexibleArgumentParser +import dynamo.nixl_connect as connect from dynamo.runtime import DistributedRuntime, dynamo_worker from dynamo.runtime.logging import configure_dynamo_logging sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) -import connect from utils.args import Config, base_parse_args, parse_endpoint from utils.image_loader import ImageLoader from utils.protocol import MyRequestOutput, vLLMMultimodalRequest @@ -149,7 +149,7 @@ async def generate( descriptor = connect.Descriptor(embeddings) with self._connector.create_readable(descriptor) as readable: - request.serialized_request = readable.to_serialized() + request.serialized_request = readable.metadata() # Clear the image URL as hint that the image is passed as embeddings. request.image_url = None @@ -190,7 +190,7 @@ async def async_init(self, runtime: DistributedRuntime): # Create and initialize a dynamo connector for this worker. # We'll needs this to move data between this worker and remote workers efficiently. - self._connector = connect.Connector(runtime=runtime, namespace=parsed_namespace) + self._connector = connect.Connector() await self._connector.initialize() logger.info("Startup completed.") diff --git a/examples/multimodal/components/worker.py b/examples/multimodal/components/worker.py index 3409cfa1dd..5b0a9faf95 100644 --- a/examples/multimodal/components/worker.py +++ b/examples/multimodal/components/worker.py @@ -32,12 +32,12 @@ from vllm.utils import FlexibleArgumentParser from vllm.v1.engine.async_llm import AsyncLLM +import dynamo.nixl_connect as connect from dynamo.llm import ZmqKvEventPublisher, ZmqKvEventPublisherConfig from dynamo.runtime import Component, DistributedRuntime, Endpoint, dynamo_worker from dynamo.runtime.logging import configure_dynamo_logging sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) -import connect from publisher import StatLoggerFactory from utils.args import ( Config, @@ -250,7 +250,7 @@ async def async_init(self, runtime: DistributedRuntime): # Create and initialize a dynamo connector for this worker. # We'll needs this to move data between this worker and remote workers efficiently. parsed_namespace, _, _ = parse_endpoint(self.endpoint) - self._connector = connect.Connector(runtime=runtime, namespace=parsed_namespace) + self._connector = connect.Connector() await self._connector.initialize() # embeddings_shape, self.embeddings_dtype = get_vision_embeddings_info( diff --git a/examples/multimodal/connect/README.md b/examples/multimodal/connect/README.md deleted file mode 100644 index e75418319f..0000000000 --- a/examples/multimodal/connect/README.md +++ /dev/null @@ -1,342 +0,0 @@ - - -# Dynamo Connect - -Dynamo connect provides a Pythonic interface to the NIXL base RDMA subsystem via a set of Python classes. -The primary goal of this library to simplify the integration of NIXL based RDMA into inference applications. - -All operations using the Connect library begin with the [`Connector`](#connector) class and the type of operation required. -There are four types of supported operations: - - - **Register local readable memory**: - - Register local memory buffer(s) with the RDMA subsystem to enable a remote worker to read from. - - - **Register local writable memory**: - - Register local memory buffer(s) with the RDMA subsystem to enable a remote worker to write to. - - - **Read from registered, remote memory**: - - Read remote memory buffer(s), registered by a remote worker to be readable, into local memory buffer(s). - - - **Write to registered, remote memory**: - - Write local memory buffer(s) to remote memory buffer(s) registered by a remote worker to writable. - -By connecting correctly paired operations, high-throughput GPU Direct RDMA data transfers can be completed. -Given the list above, the correct pairing of operations would be 1 & 3 or 2 & 4. -Where one side is a "(read|write)-able operation" and the other is its correctly paired "(read|write) operation". -Specifically, a read operation must be paired with a readable operation, and a write operation must be paired with a writable operation. - -## Examples - -### Generic Example - -In the diagram below, Local creates a [`WritableOperation`](#writableoperation) intended to receive data from Remote. -Local then sends metadata about the requested RDMA operation to Remote. -Remote then uses the metadata to create a [`WriteOperation`](#writeoperation) which will perform the GPU Direct RDMA memory transfer from Remote's GPU memory to Local's GPU memory. - -```mermaid ---- -title: Write Operation Between Two Workers ---- -flowchart LR - c1[Remote] --"3: .begin_write()"--- WriteOperation - WriteOperation e1@=="4: GPU Direct RDMA"==> WritableOperation - WritableOperation --"1: .create_writable()"--- c2[Local] - c2 e2@--"2: RDMA Metadata via HTTP"--> c1 - e1@{ animate: true; } - e2@{ animate: true; } -``` - -### Multimodal Example - -In the case of the [Dynamo Multimodal Disaggregated Example](../README.md): - - 1. The HTTP frontend accepts a text prompt and a URL to an image. - - 2. The prompt and URL are then enqueued with the Processor before being dispatched to the first available Decode Worker. - - 3. Decode Worker then requests a Prefill Worker to provide key-value data for the LLM powering the Decode Worker. - - 4. Prefill Worker then requests that the image be processed and provided as embeddings by the Encode Worker. - - 5. Encode Worker acquires the image, processes it, performs inference on the image using a specialized vision model, and finally provides the embeddings to Prefill Worker. - - 6. Prefill Worker receives the embeddings from Encode Worker and generates a key-value cache (KV$) update for Decode Worker's LLM and writes the update directly to the GPU memory reserved for the data. - - 7. Finally, Decode Worker performs the requested inference. - -```mermaid ---- -title: Multimodal Disaggregated Workflow ---- -flowchart LR - p0[HTTP Frontend] i0@--"text prompt"-->p1[Processor] - p0 i1@--"url"-->p1 - p1 i2@--"prompt"-->dw[Decode Worker] - p1 i3@--"url"-->dw - dw i4@--"prompt"-->pw[Prefill Worker] - dw i5@--"url"-->pw - pw i6@--"url"-->ew[Encode Worker] - ew o0@=="image embeddings"==>pw - pw o1@=="kv_cache updates"==>dw - dw o2@--"inference results"-->p0 - - i0@{ animate: true; } - i1@{ animate: true; } - i2@{ animate: true; } - i3@{ animate: true; } - i4@{ animate: true; } - i5@{ animate: true; } - i6@{ animate: true; } - o0@{ animate: true; } - o1@{ animate: true; } - o2@{ animate: true; } -``` - - _Note: In this example, it is the data transfer between the Prefill Worker and the Encode Worker that utilizes the Dynamo Connect library. The KV Cache transfer between Decode Worker and Prefill Worker utilizes the NIXL base RDMA subsystem directly without using the Dynamo Connect library._ - -#### Code Examples - -See [prefill_worker](../components/worker.py) or [decode_worker](../components/worker.py), -for how they coordinate directly with the Encode Worker by creating a [`WritableOperation`](#writableoperation), -sending the operation's metadata via Dynamo's round-robin dispatcher, and awaiting the operation for completion before making use of the transferred data. - -See [encode_worker](../components/encode_worker.py#L190), -for how the resulting embeddings are registered with the RDMA subsystem by creating a [`Descriptor`](#descriptor), -a [`WriteOperation`](#writeoperation) is created using the metadata provided by the requesting worker, -and the worker awaits for the data transfer to complete for yielding a response. - -## Python Classes - -### Connector - -Core class for managing the connection between workers in a distributed environment. -Use this class to create readable and writable operations, or read and write data to remote workers. - -This class is responsible for interfacing with the NIXL-based RDMA subsystem and providing a "Pythonic" interface -with which to utilize GPU Direct RDMA accelerated data transfers between models hosted by different workers in a Dynamo pipeline. -The connector provides two methods of moving data between workers: - - - Preparing local memory to be written to by a remote worker. - - - Preparing local memory to be read by a remote worker. - -In both cases, local memory is registered with the NIXL-based RDMA subsystem via the [`Descriptor`](#descriptor) class and provided to the connector. -The connector then configures the RDMA subsystem to expose the memory for the requested operation and returns an operation control object. -The operation control object, either a [`ReadableOperation`](#readableoperation) or a [`WritableOperation`](#writableoperation), -provides RDMA metadata via its [`.to_serialized()`](#to_serialized) method as well as functionality to know when the operation has been completed or cancel the operation prior to completion. - -The RDMA metadata must be provided to the remote worker expected to complete the operation. -The metadata contains required information (identifiers, keys, etc.) which enables the remote worker to interact with the provided memory. - -#### Methods - -##### `begin_read` - -> Creates a [`ReadOperation`](#readoperation) for transferring data from a remote worker. -> -> To create the operation, the serialized request from a remote worker's [`ReadableOperation`](#readableoperation) -> along with a matching set of local memory descriptors which reference memory intended to receive data from the remote worker -> must be provided. -> The serialized request must be transferred from the remote to the local worker via a secondary channel, most likely HTTP or TCP+NATS. -> -> Once created, the operation will begin reading immediately. -> Disposal of the object reference will instruct the RDMA subsystem to cancel the read operation, -> therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - -##### `begin_write` - -> Creates a write operation for transferring data to a remote worker. -> -> To create the operation, the serialized request from a remote worker's [`WritableOperation`](#writableoperation) -> along with a matching set of local memory descriptors which reference memory to be transferred to the remote worker -> must be provided. -> The serialized request must be transferred from the remote to the local worker via a secondary channel, most likely HTTP or TCP+NATS. -> -> Once created, the operation will begin writing immediately. -> Disposal of the object reference will instruct the RDMA subsystem to cancel the write operation, -> therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - -##### `create_readable` - -> Creates a [`ReadableOperation`](#readableoperation) for transferring data to a remote worker. -> -> To create the operation, a set of local memory descriptors must be provided that reference memory intended to be transferred to -> a remote worker. -> Once created, the memory referenced by the provided descriptors becomes immediately readable by a remote worker with the necessary metadata. -> The metadata required to access the memory referenced by the provided descriptors is accessible via the operations `.to_serialized()` method. -> Once acquired, the metadata needs to be provided to a remote worker via a secondary channel, most likely HTTP or TCP+NATS. -> -> Disposable of the operation's object reference will instruct the RDMA subsystem to cancel the operation, -> therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - -##### `create_writable` - -> Creates a [`WritableOperation`](#writableoperation) for transferring data from a remote worker. -> -> To create the operation, a set of local memory descriptors must be provided which reference memory intended to receive data from -> a remote worker. -> Once created, the memory referenced by the provided descriptors becomes immediately writable by a remote worker with the necessary metadata. -> The metadata required to access the memory referenced by the provided descriptors is accessible via the operations `.to_serialized()` method. -> Once acquired, the metadata needs to be provided to a remote worker via a secondary channel, most likely HTTP or TCP+NATS. -> -> Disposable of the operation's object reference will instruct the RDMA subsystem to cancel the operation, -> therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - - -### Descriptor - -Memory descriptor that ensures memory is registered with the NIXL base RDMA subsystem. -Memory must be registered with the RDMA subsystem to enable interaction with the memory. - -Descriptor objects are administrative and do not copy, move, or otherwise modify the registered memory. - -There are four ways to create a descriptor: - - 1. From a `torch.Tensor` object. Device information will be derived from the provided object. - - 2. From a `tuple` containing either a NumPy or CuPy `ndarray` and information describing where the memory resides (Host/CPU vs GPU). - - 3. From a Python `bytes` object. Memory is assumed to reside in CPU addressable host memory. - - 4. From a `tuple` comprised of the address of the memory, its size in bytes, and device information. - An optional reference to a Python object can be provided to avoid garbage collection issues. - - -### Device - -Device describes the device, or kind of memory, a given allocation resides in. -Usually host (`"cpu"`) or GPU (`"cuda"`) memory. - -When a system contains multiple GPU devices, specific GPU devices can be identified by including their ordinal index number. -For example, to reference the second GPU in a system `"cuda:1"` can be used. - -By default, when `"cuda"` is provided, it is assumed to be `"cuda:0"` or the first GPU enumerated by the system. - - -### ReadOperation - -An operation which transfers data from a remote worker to the local worker. - -To create the operation, RDMA metadata ([`SerializedRequest`](#serializedrequest)) from a remote worker's [`ReadableOperation`](#readableoperation) -along with a matching set of local [`Descriptor`](#descriptor) objects which reference memory intended to receive data from the remote worker must be provided. -The RDMA metadata must be transferred from the remote to the local worker via a secondary channel, most likely HTTP or TCP+NATS. - -Once created, the operation will begin reading immediately. -Disposal of the object reference will instruct the RDMA subsystem to cancel the read operation, -therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - -#### Methods - -##### `cancel` - -> Instructs the RDMA subsystem to cancel the operation. -> Completed operations cannot be cancelled. - -##### `wait_for_completion` - -> Blocks the caller until the memory from the remote worker has been transferred to the provided buffers. - - -### ReadableOperation - -An operation which enables a remote worker to read data from the local worker. - -To create the operation, a set of local [`Descriptor`](#descriptor) objects must be provided that reference memory intended to be transferred to a remote worker. -Once created, the memory referenced by the provided descriptors becomes immediately readable by a remote worker with the necessary metadata. -The metadata required to access the memory referenced by the provided descriptors is accessible via the operations `.to_serialized()` method. -Once acquired, the metadata needs to be provided to a remote worker via a secondary channel, most likely HTTP or TCP+NATS. - -Disposal of the operation's object reference will instruct the RDMA subsystem to cancel the operation, -therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - -#### Methods - -##### `to_serialized` - -> Generates and returns the RDMA metadata ([`SerializedRequest`](#serializedrequest)) required for a remote worker to read from the operation. -> Once acquired, the metadata needs to be provided to a remote worker via a secondary channel, most likely HTTP or TCP+NATS. - -##### `wait_for_completion` - -> Blocks the caller until the operation has received a completion signal from a remote worker. - - -### WriteOperation - -An operation which transfers data from the local worker to a remote worker. - -To create the operation, RDMA metadata ([`SerializedRequest`](#serializedrequest)) from a remote worker's [`WritableOperation`](#writableoperation) -along with a matching set of local [`Descriptor`](#descriptor) objects which reference memory to be transferred to the remote worker must be provided. -The RDMA metadata must be transferred from the remote to the local worker via a secondary channel, most likely HTTP or TCP+NATS. - -Once created, the operation will begin writing immediately. -Disposal of the object reference will instruct the RDMA subsystem to cancel the write operation, -therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - -#### Methods - -##### `cancel` - -> Instructs the RDMA subsystem to cancel the operation. -> Completed operations cannot be cancelled. - -##### `wait_for_completion` - -> Blocks the caller until all provided buffers have been transferred to the remote worker. - - -### WritableOperation - -An operation which enables a remote worker to write data to the local worker. - -To create the operation, a set of local [`Descriptor`](#descriptor) objects must be provided which reference memory intended to receive data from a remote worker. -Once created, the memory referenced by the provided descriptors becomes immediately writable by a remote worker with the necessary metadata. -The metadata required to access the memory referenced by the provided descriptors is accessible via the operations `.to_serialized()` method. -Once acquired, the metadata needs to be provided to a remote worker via a secondary channel, most likely HTTP or TCP+NATS. - -Disposal of the operation's object reference will instruct the RDMA subsystem to cancel the operation, -therefore the operation should be awaited until complete or and deleted prior to completion when cancellation is intended. - -#### Methods - -##### `to_serialized` - -> Generates and returns the RDMA metadata ([`SerializedRequest`](#serializedrequest)) required for a remote worker to write to the operation. -> Once acquired, the metadata needs to be provided to a remote worker via a secondary channel, most likely HTTP or TCP+NATS. - -##### `wait_for_completion` - -> Blocks the caller until the operation has received a completion signal from a remote worker. - - -### SerializedRequest - -A Pydantic type intended to provide JSON serialized RDMA metadata about a [`ReadableOperation`](#readableoperation) or [`WritableOperation`](#writableoperation) object. - -Use the [`.to_serialized()`](#to_serialized) method on either of the above types to generate a `SerializedRequest` object for an operation. - -## References - - - [NVIDIA Dynamo](https://developer.nvidia.com/dynamo) @ [GitHub](https://github.com/ai-dynamo/dynamo) - - [NVIDIA Inference Transfer Library (NIXL)](https://developer.nvidia.com/blog/introducing-nvidia-dynamo-a-low-latency-distributed-inference-framework-for-scaling-reasoning-ai-models/#nvidia_inference_transfer_library_nixl_low-latency_hardware-agnostic_communication%C2%A0) @ [GitHub](https://github.com/ai-dynamo/nixl) - - [Dynamo Multimodal Example](../../../examples/multimodal) - - [NVIDIA GPU Direct](https://developer.nvidia.com/gpudirect) diff --git a/examples/multimodal/connect/__init__.py b/examples/multimodal/connect/__init__.py deleted file mode 100644 index 17fdb55bbd..0000000000 --- a/examples/multimodal/connect/__init__.py +++ /dev/null @@ -1,1472 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import asyncio -import logging -import socket -import uuid -import zlib -from abc import ABC, abstractmethod -from enum import IntEnum -from functools import cached_property -from typing import Any, List, Optional - -import nixl._api as nixl_api -import nixl._bindings as nixl_bindings -import torch -from pydantic import BaseModel, ConfigDict, field_validator - -from dynamo.runtime import DistributedRuntime - -logger = logging.getLogger(__name__) - -try: - import cupy as array_module - from cupy_backends.cuda.api.runtime import CUDARuntimeError - - logger.info("Utilizing cupy to enable GPU acceleration.") -except ImportError: - try: - import numpy as array_module - - logger.warning("Failed to load cupy for GPU acceleration, utilizing numpy to provide CPU based operations.") - except ImportError as e: - raise ImportError("Numpy or cupy must be installed to use this module.") from e - - -class AbstractOperation(ABC): - """ - Abstract base class for awaitable NIXL based RDMA operations. - """ - - def __init__( - self, - connector: Connector, - operation_kind: OperationKind, - local_descriptors: Descriptor | list[Descriptor], - remote_descriptors: Optional[Descriptor | list[Descriptor]], - notification_key: Optional[str], - ) -> None: - if not isinstance(connector, Connector): - raise TypeError("Argument `connector` must be `dynamo.connect.Connector`.") - if operation_kind is not OperationKind.READ and operation_kind is not OperationKind.WRITE: - raise ValueError("Argument `operation_kind` must be either `READ` or `WRITE`.") - if not ( - isinstance(local_descriptors, (Descriptor, list)) - or (isinstance(local_descriptors, list) and all(isinstance(d, Descriptor) for d in local_descriptors)) - ): - raise TypeError("Argument `local_descriptors` must be `dynamo.connect.Descriptor` or `list[dynamo.connect.Descriptor]`.") - if ( - remote_descriptors is not None - and not ( - isinstance(remote_descriptors, Descriptor) - or (isinstance(remote_descriptors, list) and all(isinstance(d, Descriptor) for d in remote_descriptors)) - ) - ): - raise TypeError("Argument `remote_descriptors` must be dynamo.connect.Descriptor`, `list[dynamo.connect.Descriptor]`, or `None`.") - - if isinstance(local_descriptors, list) and len(local_descriptors) == 0: - raise ValueError("Argument `local_descriptors` must not be an empty list.") - if ( - remote_descriptors is not None - and isinstance(remote_descriptors, list) - and len(remote_descriptors) == 0 - ): - raise ValueError("Argument `remote_descriptors` must not be an empty list.") - - notification_key = str(uuid.uuid4()) if notification_key is None else notification_key - if not isinstance(notification_key, str): - raise TypeError("Argument `notification_key` must be `str` or `None`.") - if len(notification_key) == 0: - raise ValueError("Argument `notification_key` must not be an empty string.") - - self._notification_key: str = notification_key - self._connector: Connector = connector - self._operation_kind: OperationKind = operation_kind - self._local_descriptors: Descriptor | list[Descriptor] = local_descriptors - self._local_dlist: Optional[list[tuple[int, int, int]]] = None - self._local_memtype: DeviceKind = DeviceKind.UNSPECIFIED - self._remote_descriptors: Optional[Descriptor | list[Descriptor]] = None if remote_descriptors is None else remote_descriptors - self._remote_dlist: Optional[list[tuple[int, int, int]]] = None - self._remote_memtype: DeviceKind = DeviceKind.UNSPECIFIED - - # Register local descriptors with NIXL. - # Note: Only local descriptors should be registered with NIXL, - if isinstance(local_descriptors, list): - for d in local_descriptors: - d.register_memory(self._connector) - else: - local_descriptors.register_memory(self._connector) - - # Record local descriptors. - memtype, dtlist = self._create_dlist(local_descriptors) - self._local_dlist = dtlist - self._local_memtype = memtype - - # Record remote descriptors when provided. - if remote_descriptors is not None: - memtype, dtlist = self._create_dlist(remote_descriptors) - self._remote_dlist = dtlist - self._remote_memtype = memtype - - def __del__(self) -> None: - self._release() - - def __enter__(self) -> AbstractOperation: - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - self._release() - - def _release(self) -> None: - """ - Private method to release resources. Only to be called by `self`. - """ - pass - - @property - def connector(self) -> Connector: - """ - Gets the local associated with this operation. - """ - return self._connector - - @property - def operation_kind(self) -> OperationKind: - """ - Gets the kind of operation. - """ - return self._operation_kind - - @abstractmethod - async def wait_for_completion(self) -> None: - """ - Blocks the caller asynchronously until the operation has completed. - """ - raise NotImplementedError("Abstract method not implemented by derived class.") - - # Private Methods - - def _create_dlist( - self, - descriptors: Descriptor | list[Descriptor], - ) -> tuple[DeviceKind, list[tuple[int, int, int]]]: - """ - Helper function to create a list of tuples (ptr, size, device) from descriptors. - """ - dlist: list[tuple[int, int, int]] = [] - memtype: DeviceKind = DeviceKind.UNSPECIFIED - if isinstance(descriptors, list): - memtype = descriptors[0].device.kind - for desc in descriptors: - if memtype != desc.device.kind: - raise ValueError("All local descriptors must have the same memory type.") - dlist.append((desc.ptr, desc.size, desc.device.id)) - else: - memtype = descriptors.device.kind - dlist.append((descriptors.ptr, descriptors.size, descriptors.device.id)) - return (memtype, dlist) - - -class ActiveOperation(AbstractOperation): - """ - Abstract class for active operations that initiates a NIXL based RDMA transfer based `SerializedRequest` - provided by the remote worker's corresponding `PassiveOperation`. - """ - - def __init__( - self, - remote: Remote, - operation_kind: OperationKind, - local_descriptors: Descriptor | list[Descriptor], - remote_descriptors: Descriptor | list[Descriptor], - notification_key: str, - ) -> None: - if not isinstance(remote, Remote) or remote._connector is None: - raise TypeError("Argument `remote` must be valid `dynamo.connect.Remote`.") - if not isinstance(operation_kind, OperationKind): - raise TypeError("Argument `operation_kind` must `dynamo.connect.OperationKind`.") - if operation_kind is not OperationKind.READ and operation_kind is not OperationKind.WRITE: - raise ValueError("Argument `operation_kind` must be either `READ` or `WRITE`.") - if not ( - isinstance(local_descriptors, Descriptor) - or (isinstance(local_descriptors, list) and all(isinstance(d, Descriptor) for d in local_descriptors)) - ): - raise TypeError("Argument `local_descriptors` must be `dynamo.connect.Descriptor` or `list[dynamo.connect.Descriptor]`.") - if not ( - isinstance(remote_descriptors, Descriptor) - or (isinstance(remote_descriptors, list) and all(isinstance(d, Descriptor) for d in remote_descriptors)) - ): - raise TypeError("Argument `remote_descriptors` must be `dynamo.connect.Descriptor` or `list[dynamo.connect.Descriptor]`.") - - # Unpack single descriptors from lists if they are provided as single descriptors. - if isinstance(local_descriptors, list) and len(local_descriptors) == 1: - local_descriptors = local_descriptors[0] - if isinstance(remote_descriptors, list) and len(remote_descriptors) == 1: - remote_descriptors = remote_descriptors[0] - - if (isinstance(local_descriptors, list) and isinstance(remote_descriptors, list) and len(local_descriptors) != len(remote_descriptors)): - raise ValueError("When `local_descriptors` and `remote_descriptors` are lists, they must have the same length.") - elif isinstance(local_descriptors, list) != isinstance(remote_descriptors, list): - raise ValueError("Both `local_descriptors` and `remote_descriptors` must be either lists or single descriptors.") - if not isinstance(notification_key, str): - raise TypeError("Argument `notification_key` must be `str`.") - if len(notification_key) == 0: - raise ValueError("Argument `notification_key` must not be an empty string.") - - self._remote = remote - self._status = OperationStatus.UNINTIALIZED - - super().__init__(remote.connector, operation_kind, local_descriptors, remote_descriptors, notification_key) - # Quick check to ensure remote descriptors are not None to make static analysis happy. - if self._local_dlist is None or self._remote_dlist is None: - raise RuntimeError("NIXL descriptor list(s) not bound to operation.") - - self._local_xfer_descs: Optional[nixl_bindings.nixlXferDList] = None - self._remote_xfer_descs: Optional[nixl_bindings.nixlXferDList] = None - self._xfer_hndl: Optional[nixl_api.nixl_xfer_handle] = None - - self._local_xfer_descs = self._connector._nixl.get_xfer_descs( - descs=self._local_dlist, - mem_type=str(self._local_memtype), - ) - logger.debug(f"Created local NIXL xfer descs: {self._local_xfer_descs}") - self._remote_xfer_descs = self._connector._nixl.get_xfer_descs( - descs=self._remote_dlist, - mem_type=str(self._remote_memtype), - ) - logger.debug(f"Created remote NIXL xfer descs: {self._remote_xfer_descs}") - self._xfer_hndl = self._connector._nixl.initialize_xfer( - operation=str(operation_kind), - local_descs=self._local_xfer_descs, - remote_descs=self._remote_xfer_descs, - remote_agent=self._remote.name, - notif_msg=self._notification_key.encode("utf-8"), - ) - logger.debug(f"Created NIXL transfer handle: {self._xfer_hndl}") - - def __del__(self) -> None: - super().__del__() - self._release() - - def __enter__(self) -> ActiveOperation: - super().__enter__() - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - match self.status: - case OperationStatus.IN_PROGRESS | OperationStatus.INITIALIZED: - self._status = OperationStatus.CANCELLED - - self._release() - - def __repr__(self) -> str: - return str( - f"{self.__class__.__name__}(" - f"operation_kind={self._operation_kind}, " - f"local_descriptors={self._local_descriptors}, " - f"remote_descriptors={self._remote_descriptors}, " - f"notification_key='{self._notification_key}', " - f"remote='{self._remote.name}', " - f"status='{self._status}'" - f")" - ) - - def _release(self) -> None: - """ - Private method to release resources. - """ - error: Optional[Exception] = None - - if self._xfer_hndl is not None: - try: - logger.debug(f"NIXL transfer handle {self._xfer_hndl} released.") - self._connector._nixl.release_xfer_handle(self._xfer_hndl) - except Exception as e: - logger.error(f"Failed to release resources: {e}") - error = e - finally: - self._xfer_hndl = None - - try: - super()._release() - except Exception as e: - logger.error(f"Failed to release WaitableOperation resources: {e}") - if error is not None: - e.__cause__ = error - error = e - - if error is not None: - raise error - - def _cancel_(self) -> None: - if self._xfer_hndl is None: - return - if self.status == OperationStatus.ERRORED: - raise RuntimeError("Operation is errored, unable to cancel the operation.") - - logger.info(f"Cancellation requested for operation {{ kind={self._operation_kind}, remote='{self._remote.name}', status={self._status} }}.") - - # NIXL will cancel the transfer if it is in progress when the handle is released. - self._connector._nixl.release_xfer_handle(self._xfer_hndl) - self._status = OperationStatus.CANCELLED - self._xfer_hndl = None - - async def _wait_for_completion_(self) -> None: - # Loop until the operation is no longer in progress (or "initalized"), - # yielding control to the event loop to allow other operations to run. - iteration_count = 0 - while True: - if iteration_count % 10 == 0: - logger.debug(f"Waiting for operation {{ kind={self._operation_kind}, remote='{self._remote.name}', duration={iteration_count / 10}s }}.") - match self.status: - # "in progress" or "initialized" means the operation is ongoing. - case OperationStatus.INITIALIZED: - await asyncio.sleep(0.1) - case OperationStatus.IN_PROGRESS: - await asyncio.sleep(0.1) - # Any other state indicates completion or error. - case _: - return - iteration_count += 1 - - @abstractmethod - def cancel(self) -> None: - """ - Cancels the operation. - No affect if the operation has already completed or errored, or has been cancelled. - """ - raise NotImplementedError("Abstract method not implemented by derived class.") - - @property - def remote(self) -> Remote: - """ - Gets the remote worker associated with this operation. - """ - return self._remote - - @property - def status(self) -> OperationStatus: - """ - Gets the status of the operation. - """ - # Early return if the operation is already complete, errored, or cancelled. - match self._status: - case OperationStatus.COMPLETE | OperationStatus.ERRORED | OperationStatus.CANCELLED: - return self._status - - if self._xfer_hndl is None: - raise RuntimeError("NIXL transfer handle is invalid.") - - old_status = self._status - - if self._status == OperationStatus.UNINTIALIZED: - state = self._connector._nixl.transfer(self._xfer_hndl, self._notification_key.encode("utf-8")) - logger.debug(f"NIXL reported transfer state: {state}") - if state == "ERR": - self._status = OperationStatus.ERRORED - elif state == "DONE": - self._status = OperationStatus.COMPLETE - else: - self._status = OperationStatus.INITIALIZED - else: - state = self._connector._nixl.check_xfer_state(self._xfer_hndl) - logger.debug(f"NIXL reported transfer state: {state}") - if state == "ERR": - self._status = OperationStatus.ERRORED - elif state == "DONE": - self._status = OperationStatus.COMPLETE - else: - self._status = OperationStatus.IN_PROGRESS - - if self._status != old_status: - logger.debug(f"{self.__class__.__name__} {{ remote: '{self._remote.name}' status: '{old_status}' => '{self._status}' }}.") - - return self._status - - -class Connector: - """ - Core class for managing the connection between workers in a distributed environment. - Use this class to create readable and writable operations, or read and write data to remote workers. - """ - - def __init__( - self, - namespace: Optional[str] = None, - runtime: Optional[DistributedRuntime] = None, - worker_id: Optional[str] = None, - ) -> None: - """ - Creates a new Connector instance. - - Parameters - ---------- - namespace : Optional[str], optional - Dynamo namespace of the component, defaults to "dynamo" when `None`. - runtime : Optional[DistributedRuntime], optional - Reference the dynamo runtime used by the compenent, attempts to use the current runtime when `None`. - worker_id : Optional[str], optional - Unique identifier of the worker, defaults to a new UUID when `None`. - - Raises - ------ - TypeError - When `namespace` is provied and not of type 'str'. - TypeError - When `runtime` iis provied and not of type `dynamo.runtime.DistributedRuntime`. - TypeError - When `worker_id` is provied and not of type `uuid.UUID`. - """ - namespace = "dynamo" if namespace is None else namespace - if not isinstance(namespace, str): - raise TypeError("Argument `namespace` must be `str` or `None`.") - if not isinstance(runtime, DistributedRuntime) or runtime is None: - raise TypeError("Argument `runtime` must be `dynamo.runtime.DistributedRuntime` or `None`.") - worker_id = worker_id if worker_id is not None else str(uuid.uuid4()) - if not isinstance(worker_id, str) or len(worker_id) == 0: - raise TypeError("Argument `worker_id` must be a non-empty `str` or `None`.") - - self._worker_id = worker_id - self._is_initialized = False - self._runtime = runtime - self._namespace = namespace - self._nixl = nixl_api.nixl_agent(self._worker_id) - self._hostname = socket.gethostname() - self._agent_metadata: Optional[bytes] = None - - logger.debug(f"Created {self.__repr__()}.") - - def __repr__(self) -> str: - return str( - f"{self.__class__.__name__}(" - f"worker_id='{self._worker_id}', " - f"namespace={self._namespace}, " - f"hostname={self._hostname}, " - f"metadata=<{0 if self._agent_metadata is None else len(self._agent_metadata)} bytes>" - ")" - ) - - def __str__(self) -> str: - return self._worker_id - - @cached_property - def is_cuda_available(self) -> bool: - # Note: cuda.is_avalailable initializes cuda - # and can't be called when forking subprocesses - # care should be taken to only call it within - # subprocesses or use 'spawn' - try: - return array_module.cuda is not None and array_module.cuda.is_available() - except CUDARuntimeError: - return False - - @property - def metadata(self) -> bytes: - """ - Get the metadata of the worker. - """ - return self._nixl.get_agent_metadata() - - @property - def name(self) -> str | None: - """ - Get the name of the worker. - """ - return self._worker_id - - @property - def namespace(self) -> str: - """ - Get the namespace of the local. - """ - return self._namespace - - @property - def runtime(self) -> DistributedRuntime: - """ - Get the runtime of the local. - """ - if self._runtime is None: - raise RuntimeError("Runtime is not set. This Connector was not initialized with a runtime.") - return self._runtime - - async def begin_read( - self, - remote_request: SerializedRequest, - local_descriptors: Descriptor | list[Descriptor], - ) -> ReadOperation: - """ - Creates a read operation for fulfilling a remote readable operation. - - Parameters - ---------- - remote_request : SerializedRequest - Serialized request from a remote worker that has created a readable operation. - local_descriptors : Descriptor | list[Descriptor] - Local descriptor(s) to receive data from the remote worker described by `remote_request`. - - Returns - ------- - ReadOperation - Awaitable read operation that can be used to transfer data from a remote worker. - - Raises - ------ - TypeError - When `remote_request` is not of type `SerializedRequest`. - TypeError - When `local_descriptors` is not of type `dynamo.connect.Descriptor` or `list[dynamo.connect.Descriptor]`. - """ - if remote_request is None or not isinstance(remote_request, SerializedRequest): - raise TypeError("Argument `remote_request` must be `SerializedRequest`.") - if not ( - isinstance(local_descriptors, Descriptor) - or (isinstance(local_descriptors, list) and all(isinstance(d, Descriptor) for d in local_descriptors)) - ): - raise TypeError("Argument `local_descriptors` must be `dynamo.connect.Descriptor` or `list[dynamo.connect.Descriptor]`.") - if remote_request.operation_kind != OperationKind.READ.value: - raise RuntimeError("Cannot create a `dynamo.connect.ReadOperation` to read from a remote `dynamo.connect.WritableOperation`.") - - if not self._is_initialized: - raise RuntimeError("Connector not initialized. Call `initialize()` before calling this method.") - - op = ReadOperation(self, remote_request, local_descriptors) - return op - - async def begin_write( - self, - local_descriptors: Descriptor | list[Descriptor], - remote_request: SerializedRequest, - ) -> WriteOperation: - """ - Creates a write operation for transferring data to a remote worker. - - Parameters - ---------- - remote_request : SerializedRequest - Serialized request from a remote worker that has created a readable operation. - local_descriptors : Descriptor | list[Descriptor] - Local descriptors of one or more data objects to be transferred to the remote worker. - """ - if remote_request is None or not isinstance(remote_request, SerializedRequest): - raise TypeError("Argument `remote_request` must be `SerializedRequest`.") - if not ( - isinstance(local_descriptors, Descriptor) - or (isinstance(local_descriptors, list) and all(isinstance(d, Descriptor) for d in local_descriptors)) - ): - raise TypeError("Argument `local_descriptors` must be `Descriptor` or `list[Descriptor]`.") - if remote_request.operation_kind != OperationKind.WRITE: - raise RuntimeError("Cannot create a `WriteOperation` to write to a remote `ReadableOperation`.") - if not isinstance(remote_request.nixl_metadata, str): - raise TypeError("Argument `remote_request.nixl_metadata` must be `str`.") - - if not self._is_initialized: - raise RuntimeError("Connector not initialized. Call `initialize()` before calling this method.") - - op = WriteOperation(self, local_descriptors, remote_request) - return op - - def create_readable( - self, - local_descriptors: Descriptor | list[Descriptor], - ) -> ReadableOperation: - """ - Creates a readable operation for transferring data from a remote worker. - - Returns - ------- - ReadableOperation - A readable operation that can be used to transfer data from a remote worker. - """ - if not self._is_initialized: - raise RuntimeError("Connector not initialized. Call `initialize()` before calling this method.") - - op = ReadableOperation(self, local_descriptors) - return op - - def create_writable( - self, - local_descriptors: Descriptor | list[Descriptor], - ) -> WritableOperation: - """ - Creates a writable operation for transferring data to a remote worker. - - Returns - ------- - WritableOperation - A writable operation that can be used to transfer data to a remote worker. - """ - if not self._is_initialized: - raise RuntimeError("Connector not initialized. Call `initialize()` before calling this method.") - - op = WritableOperation(self, local_descriptors) - return op - - async def initialize(self) -> None: - # Only initialize the connector once. - if self._is_initialized: - return - - self._is_initialized = True - # This method is a no-op for now, in the future it may be used to initialize the connector. - logger.debug(f"Initialized Connector {{ name: '{self._worker_id}', namespace '{self._namespace}' }} completed.") - - -class Descriptor: - """ - Memory descriptor that ensures memory is registered w/ NIXL, used for transferring data between workers. - """ - - def __init__( - self, - data: torch.Tensor | tuple[array_module.ndarray, Device|str] | bytes | tuple[int, int, Device|str, Any], - ) -> None: - """ - Memory descriptor for transferring data between workers. - - Parameters - ---------- - data : torch.Tensor | tuple[ndarray, Device|str] | bytes | tuple[int, int, Device|str, Any] - The data to be transferred. - - When `torch.Tensor` is provided, the attributes of the tensor will be used to create the descriptor. - - When `tuple[ndarray, Device]` is provided, the tuple must contain: - - `ndarray`: The CuPy or NumPy array to be transferred. - - `Device`: Either a `dynamo.connect.Device` or a string representing the device type (e.g., "cuda" or "cpu"). - - When `bytes` is provided, the pointer and size derived from the bytes object and memory type will be assumed to be CPU. - - When `tuple[int, int, Device|str, Any]` is provided, the tuple must contain the following elements: - - `int`: Pointer to the data in memory. - - `int`: Size of the data in bytes. - - `Device`: Either a `dynamo.connect.Device` or a string representing the device type (e.g., "cuda" or "cpu"). - - `Any`: Optional reference to the data (e.g., the original tensor or bytes object). - This is useful for keeping a reference to the data in memory, but it is not required. - - Raises - ------ - ValueError - When `data` is `None`. - TypeError - When `data` is not a valid type (i.e., not `torch.Tensor`, `bytes`, or a valid tuple). - TypeError - When `data` is a tuple but the elements are not of the expected types (i.e., [`ndarray`, `Device|str`] OR [`int`, `int`, `Device|str`, `Any`]). - """ - TYPE_ERROR_MESSAGE = "Argument `data` must be `torch.Tensor`, `tuple[ndarray, Device|str]`, `bytes`, or `tuple[int, int, Device|str, Any]`." - if data is None: - raise ValueError("Argument `data` cannot be `None`.") - if not (isinstance(data, torch.Tensor) or isinstance(data, bytes) or isinstance(data, tuple)): - raise TypeError(TYPE_ERROR_MESSAGE) - - self._data_device: Device = Device("cpu") - self._data_ptr: int = 0 - self._data_ref: Optional[Any] = None - self._data_size: int = 0 - - # Member fields for managing NIXL memory registration. - # Note: ONLY local descriptors should be registered with NIXL, - # remote descriptors do not have a valid memory address and registration will fault. - self._connector: Optional[Connector] = None - self._nixl_hndl: Optional[nixl_bindings.nixlRegDList] = None - - # Initially `None` cached serialized descriptor reference, populated when `to_serialized()` is called. - self._serialized: Optional[SerializedDescriptor] = None - - # Data is `torch.Tensor`. - if isinstance(data, torch.Tensor): - self._data_ptr = data.data_ptr() - self._data_size = data.numel() * data.element_size() - if data.is_cuda: - self._data_device = Device((DeviceKind.CUDA, data.get_device())) - self._data_ref = data - - logger.debug(f"Created {self.__repr__()} from `torch.Tensor`.") - - # Data is `tuple[ndarray, Device]`. - elif ( - isinstance(data, tuple) - and len(data) == 2 - and isinstance(data[0], array_module.ndarray) - and (isinstance(data[1], Device) or isinstance(data[1], str)) - ): - if hasattr(data[0], "__array_interface__"): - self._data_ptr = data[0].__array_interface__["data"][0] - elif hasattr(data[0], "__cuda_array_interface__"): - self._data_ptr = data[0].__cuda_array_interface__["data"][0] - else: - raise TypeError("Argument `data[0]` must be a `ndarray` with a valid array interface.") - self._data_size = data[0].nbytes - self._data_device = data[1] if isinstance(data[1], Device) else Device(data[1]) - self._data_ref = data[0] - - logger.debug(f"Created {self.__repr__()} from `tuple[ndarray, Device|str]`.") - - # Data is `bytes`. - elif isinstance(data, bytes): - self._data_ptr = id(data) - self._data_size = len(data) - self._data_ref = data - - logger.debug(f"Created {self.__repr__()} from `bytes`.") - - # Data is `tuple[int, int, Device, dtype, tuple, Any]`. - elif isinstance(data, tuple) and len(data) >= 2 and isinstance(data[0], int) and isinstance(data[1], int): - if len(data) >= 3 and not (isinstance(data[2], Device) or isinstance(data[2], str)): - raise TypeError("Argument `data` must be a `tuple[int, int, Device|str, Any]`.") - - self._data_ptr = data[0] - self._data_size = data[1] - if len(data) >= 3: - self._data_device = data[2] if isinstance(data[2], Device) else Device(data[2]) - self._data_ref = data[3] if len(data) >=4 else None - - logger.debug(f"Created {self.__repr__()} from `tuple[int, int, Device|str, Any]`.") - else: - raise TypeError(TYPE_ERROR_MESSAGE) - - def __del__(self) -> None: - if self._nixl_hndl is not None and self._connector is not None: - # Unregister the memory with NIXL. - self._connector._nixl.deregister_memory(self._nixl_hndl) - self._nixl_hndl = None - - if self._data_ref is not None: - # Release the reference to the data. - del self._data_ref - - logger.debug(f"Deleted {self.__repr__()}.") - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self})" - - def __str__(self) -> str: - return f"ptr={hex(self._data_ptr)}, size={self._data_size}, device={self._data_device}" - - @property - def device(self) -> Device: - """ - Gets the device the of the descriptor. - """ - return self._data_device - - @property - def ptr(self) -> int: - """ - Gets the pointer of the descriptor. - """ - return self._data_ptr - - @property - def size(self) -> int: - """ - Gets the size of the descriptor. - """ - return self._data_size - - @staticmethod - def from_serialized( - serialized: SerializedDescriptor, - ) -> Descriptor: - """ - Deserializes a `SerializedDescriptor` into a `Descriptor` object. - - Parameters - ---------- - serialized : SerializedDescriptor - The serialized descriptor to deserialize. - - Returns - ------- - Descriptor - The deserialized descriptor. - """ - if not isinstance(serialized, SerializedDescriptor): - raise TypeError("Argument `serialized` must be `SerializedDescriptor`.") - - return serialized.to_descriptor() - - def register_memory( - self, - connector: Connector, - ) -> None: - """ - Registers the memory of the descriptor with NIXL. - """ - if not isinstance(connector, Connector): - raise TypeError("Argument `connector` must be `dynamo.connect.Connector`.") - if self._data_ptr == 0: - raise ValueError("Cannot register memory with a null pointer.") - - if not (self._nixl_hndl is None and self._connector is None): - return - - # Register the memory with NIXL. - self._connector = connector - if isinstance(self._data_ref, torch.Tensor): - self._nixl_hndl = connector._nixl.register_memory(self._data_ref) - else: - mem_type = str(self._data_device.kind) - reg_list = [(self._data_ptr, self._data_size, self._data_device.id, mem_type)] - self._nixl_hndl = connector._nixl.register_memory(reg_list, mem_type) - - logger.debug(f"Registered {self.__repr__()} with NIXL.") - - def to_serialized(self) -> SerializedDescriptor: - """ - Serializes the descriptor into a `SerializedDescriptor` object. - """ - if self._serialized is None: - self._serialized = SerializedDescriptor( - device=f"{self._data_device}", - ptr=self._data_ptr, - size=self._data_size, - ) - - return self._serialized - - -class Device: - """ - Represents a device in the system. - """ - - def __init__( - self, - metadata: str | tuple[DeviceKind, int], - ) -> None: - if metadata is None: - raise ValueError("Argument `metadata` cannot be `None`.") - if isinstance(metadata, tuple) and len(metadata) == 2 and isinstance(metadata[0], DeviceKind) and isinstance(metadata[1], int): - kind, device_id = metadata - elif isinstance(metadata, str): - metadata = metadata.strip().lower() - if metadata.startswith("cuda") or metadata.startswith("gpu"): - kind = DeviceKind.CUDA - device_id = 0 if metadata.find(":") == -1 else int(metadata.split(":")[1]) - elif metadata.startswith("cpu") or metadata.startswith("host"): - kind = DeviceKind.HOST - device_id = 0 - else: - raise ValueError("Argument `metadata` must be in the format 'cuda:' or 'cpu'.") - else: - raise TypeError("Argument `metadata` must be a `tuple[MemoryKind, int]` or a `str`.") - - - self._device_id = device_id - self._kind = kind - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(kind={self._kind}, id={self._device_id})" - - def __str__(self) -> str: - return f"{self._kind}:{self._device_id}" if self._kind is DeviceKind.CUDA else f"{self._kind}" - - @property - def id(self) -> int: - """ - Gets the device ID of the device. - """ - return self._device_id - - @property - def kind(self) -> DeviceKind: - """ - Gets the memory kind of the device. - """ - return self._kind - - -class DeviceKind(IntEnum): - """ - Type of memory a descriptor has been allocated to. - """ - - UNSPECIFIED = 0 - HOST = 1 - CUDA = 2 - - def __str__(self) -> str: - if self == DeviceKind.HOST: - return "cpu" - elif self == DeviceKind.CUDA: - return "cuda" - else: - return "" - - -class OperationKind(IntEnum): - """ - Kind of an operation. - """ - - UNSPECIFIED = 0 - READ = 1 - WRITE = 2 - - def __str__(self) -> str: - if self == OperationKind.READ: - return "READ" - elif self == OperationKind.WRITE: - return "WRITE" - else: - return "" - - -class OperationStatus(IntEnum): - """ - Status of an operation. - """ - - UNINTIALIZED = 0 - INITIALIZED = 1 - IN_PROGRESS = 2 - COMPLETE = 3 - CANCELLED = 4 - ERRORED = 5 - - def __str__(self) -> str: - if self == OperationStatus.INITIALIZED: - return "INIT" - elif self == OperationStatus.IN_PROGRESS: - return "PROC" - elif self == OperationStatus.COMPLETE: - return "DONE" - elif self == OperationStatus.ERRORED: - return "ERR" - elif self == OperationStatus.CANCELLED: - return "STOP" - else: - return "" - - -class PassiveOperation(AbstractOperation): - """ - Abstract class for common functionality of passive operations. - """ - - def __init__( - self, - connector: Connector, - operation_kind: OperationKind, - local_descriptors: Descriptor | list[Descriptor], - ) -> None: - if operation_kind is not OperationKind.READ and operation_kind is not OperationKind.WRITE: - raise ValueError("Argument `operation_kind` must be either `READ` or `WRITE`.") - - self._status = OperationStatus.UNINTIALIZED - - super().__init__(connector, operation_kind, local_descriptors, None, None) - - self._serialized_request: Optional[SerializedRequest] = None - self._status = OperationStatus.INITIALIZED - - def __del__(self) -> None: - super().__del__() - - def __enter__(self) -> AbstractOperation: - super().__enter__() - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - super().__exit__(exc_type, exc_value, traceback) - - def __repr__(self) -> str: - return str( - f"{self.__class__.__name__}(" - f"operation_kind={self._operation_kind}, " - f"local_descriptors={self._local_descriptors}, " - f"notification_key='{self._notification_key}', " - f"status='{self._status}'" - f")" - ) - - async def _wait_for_completion_(self) -> None: - # Loop until the operation is no longer in progress (or "initalized"), - # yielding control to the event loop to allow other operations to run. - while True: - match self.status: - # "in progress" or "initialized" means the operation is ongoing. - case OperationStatus.INITIALIZED: - await asyncio.sleep(0.1) - case OperationStatus.IN_PROGRESS: - await asyncio.sleep(0.1) - # Any other state indicates completion or error. - case _: - return - - @property - def status(self) -> OperationStatus: - """ - Gets the status of the operation. - """ - # Early return if the operation is already complete, errored, or cancelled. - match self._status: - case OperationStatus.COMPLETE | OperationStatus.ERRORED | OperationStatus.CANCELLED: - return self._status - - old_status = self._status - - # Query NIXL for any notifications. - notifications = self._connector._nixl.update_notifs() - - if isinstance(notifications, dict): - remote_state = OperationStatus.IN_PROGRESS - logger.debug(f"NIXL reported notifications: {len(notifications)}.") - - for key, values in notifications.items(): - if not isinstance(values, list): - raise TypeError(f"Expected `dict[str, list[bytes]]` from NIXL notification query; got {type(notifications)}.") - for value in values: - if not isinstance(value, bytes): - continue - notification_key = value.decode("utf-8") - - # Once we've found the notification key, we know the operation is complete. - if notification_key == self._notification_key: - remote_state = OperationStatus.COMPLETE - break - - if remote_state == OperationStatus.COMPLETE: - self._status = remote_state - logger.debug(f"{self.__class__.__name__} {{ remote: '{self._connector.name}' status: '{old_status}' => '{self._status}' }}.") - - return self._status - - def to_serialized(self) -> SerializedRequest: - """ - Gets the request descriptor for the operation. - """ - if self._serialized_request is None: - # When we've not yet cached the serialized request, we need to generate one before returning it. - # Handle both cases: multiple and single descriptors. - if isinstance(self._local_descriptors, list): - descriptors = [desc.to_serialized() for desc in self._local_descriptors] - else: - descriptors = [self._local_descriptors.to_serialized()] - - original_len = len(self._connector.metadata) - nixl_metadata = self._connector.metadata - nixl_metadata = zlib.compress(nixl_metadata, level=6) - compressed_len = len(nixl_metadata) - logger.debug(f"Compressed NIXL metadata from {original_len} bytes to {compressed_len} bytes.") - if compressed_len > original_len: - logger.warning(f"Compressed NIXL metadata is larger than original ({compressed_len} > {original_len}).") - - self._serialized_request = SerializedRequest( - descriptors=descriptors, - nixl_metadata=nixl_metadata.hex(), - notification_key=self._notification_key, - operation_kind=int(self._operation_kind), - ) - - return self._serialized_request - - @abstractmethod - async def wait_for_completion(self) -> None: - """ - Blocks the caller asynchronously until the operation has completed. - """ - raise NotImplementedError("Abstract method not implemented by derived class.") - - -class ReadOperation(ActiveOperation): - """ - Operation that initiates an RDMA read operation to transfer data from a remote worker's `ReadableOperation`, - as described by `remote_request`, to local buffers. - """ - - def __init__( - self, - connector: Connector, - remote_request: SerializedRequest, - local_descriptors: Descriptor | list[Descriptor], - ) -> None: - """ - Creates a new instance of `ReadOperation`, registers `local_descriptors` with NIXL, - and begins an RDMA read operation which will transfer data described by `remote_request` - to `local_descriptors`. - - Parameters - ---------- - connector : Connector - Connector instance to use for the operation. - remote_request : SerializedRequest - Serialized request from the remote worker. - local_descriptors : Descriptor | list[Descriptor] - Local descriptor(s) to to receive the data from the remote worker. - """ - if not isinstance(connector, Connector): - raise TypeError("Argument `connector` must be `dynamo.connect.Connector`.") - if not isinstance(remote_request, SerializedRequest): - raise TypeError("Argument `remote_request` must be `dynamo.connect.RequestDescriptor`.") - if remote_request.operation_kind != OperationKind.READ.value: - raise ValueError("Argument `remote_request` must be of kind `READ`.") - - remote = Remote(connector, remote_request.nixl_metadata) - remote_descriptors = remote_request.to_descriptors() - - if not ( - isinstance(local_descriptors, Descriptor) - or (isinstance(local_descriptors, list) and all(isinstance(d, Descriptor) for d in local_descriptors)) - ): - raise TypeError("Argument `local_descriptors` must be `dynamo.connect.Descriptor`, `list[dynamo.connect.Descriptor]`.") - - super().__init__(remote, OperationKind.READ, local_descriptors, remote_descriptors, remote_request.notification_key) - logger.debug(f"Created {self.__repr__()}") - - def __del__(self) -> None: - super().__del__() - logger.debug(f"Deleted {self.__repr__()}") - - def __enter__(self) -> ReadOperation: - super().__enter__() - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - super().__exit__(exc_type, exc_value, traceback) - - def __repr__(self) -> str: - return super().__repr__() - - def cancel(self) -> None: - """ - Cancels the operation. - No affect if the operation has already completed or errored, or been cancelled. - """ - super()._cancel_() - - def results(self) -> list[Descriptor]: - """ - Gets the results of the operation. - Returns a single descriptor if only one was requested, or a list of descriptors if multiple were requested. - """ - if self._status != OperationStatus.COMPLETE: - raise RuntimeError("Operation has not completed yet, cannot get results.") - - return self._local_descriptors if isinstance(self._local_descriptors, list) else [self._local_descriptors] - - async def wait_for_completion(self) -> None: - """ - Blocks the caller asynchronously until the operation has completed. - """ - await super()._wait_for_completion_() - - -class ReadableOperation(PassiveOperation): - """ - Operation that can be awaited until a remote worker has completed a `ReadOperation`. - """ - - def __init__( - self, - connector: Connector, - local_descriptors: Descriptor | list[Descriptor], - ) -> None: - super().__init__(connector, OperationKind.READ, local_descriptors) - logger.debug(f"Created {self.__repr__()}") - - def __del__(self) -> None: - super().__del__() - logger.debug(f"Deleted {self.__repr__()}") - - def __enter__(self) -> ReadableOperation: - super().__enter__() - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - super().__exit__(exc_type, exc_value, traceback) - - def __repr__(self) -> str: - return super().__repr__() - - async def wait_for_completion(self) -> None: - """ - Blocks the caller asynchronously until the operation has completed. - """ - await super()._wait_for_completion_() - - -class Remote: - """ - Identifies a remote NIXL enabled worker relative to a local NIXL enabled worker. - """ - - def __init__( - self, - connector: Connector, - nixl_metadata: bytes | str, - ) -> None: - if not isinstance(connector, Connector): - raise TypeError("Argument `local` must be `dynamo.connect.Connector`.") - if not (isinstance(nixl_metadata, bytes) or isinstance(nixl_metadata, str)): - raise TypeError("Argument `nixl_metadata` must be `bytes` or `str`.") - if len(nixl_metadata) == 0: - raise ValueError("Argument `nixl_metadata` cannot be empty.") - - self._connector = connector - - # When `nixl_metadata` is a string, it is assumed to have come from a remote worker - # via a `SerializedRequest` object and therefore can assumed be a hex-encoded, compressed - # representation of the NIXL metadata. - if isinstance(nixl_metadata, str): - # Decode the hex-encoded string into bytes. - nixl_metadata = bytes.fromhex(nixl_metadata) - # Decompress the NIXL metadata. - nixl_metadata = zlib.decompress(nixl_metadata) - - self._name = connector._nixl.add_remote_agent(nixl_metadata) - if isinstance(self._name, bytes): - self._name = self._name.decode("utf-8") - - logger.debug(f"Created {self.__repr__()}.") - - def __del__(self) -> None: - self._release() - - def __enter__(self) -> Remote: - """ - Context manager entry method. Returns the current instance. - """ - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - """ - Context manager exit method. Cleans up the instance. - """ - self._release() - - def __repr__(self) -> str: - return f"Remote(name={self._name}, connector={self._connector.name})" - - def __str__(self) -> str: - return self._name - - def _release(self) -> None: - """ - Private method for releasing NIXL resources. Not intended for public use. - """ - # We have to unregister the remote agent from NIXL because we cannot know if the remote worker has updated its descriptors or not, and - # NIXL will return an error if we attempt to register a remote agent with the same name but different descriptors (aka conn_info). - self._connector._nixl.remove_remote_agent(self._name) - logger.debug(f"dynamo.connect.{self.__class__.__name__}: Unregistered NIXL remote {{ name: \"{self._name}\" }}.") - - @property - def connector(self) -> Connector: - """ - Gets the local connector associated with this remote worker. - """ - return self._connector - - @property - def name(self) -> str: - """ - Gets the name of the remote worker. - """ - return self._name - - -class SerializedDescriptor(BaseModel): - """ - Pydantic serialization type for memory descriptors. - """ - model_config = ConfigDict( - extra="forbid", - frozen=True, - arbitrary_types_allowed=True, - ) - - device: str = "cpu" - ptr: int = 0 - size: int = 0 - - def to_descriptor(self) -> Descriptor: - """ - Deserialize the serialized descriptor into a `Descriptor` object. - """ - return Descriptor(data=(self.ptr, self.size, self.device, None)) - - @field_validator("device") - def validate_memtype(cls, v: str) -> str: - if not isinstance(v, str): - raise TypeError("Argument `device` must be `str`.") - v = v.strip().lower() - if not (v.startswith("cuda") or v == "cpu"): - raise ValueError("Argument `device` must be one of 'cpu' or 'cuda:'.") - return v - - @field_validator("ptr") - def validate_ptr(cls, v: int) -> int: - if v == 0: - raise ValueError("Argument `ptr` cannot be zero (aka `null` or `None`).") - return v - - @field_validator("size") - def validate_size(cls, v: int) -> int: - if v < 0: - raise ValueError("Argument `size` must be an integer greater than or equal to zero.") - return v - - -class SerializedRequest(BaseModel): - """ - Pydantic serialization type for describing the passive side of a transfer. - """ - model_config = ConfigDict( - extra="forbid", - frozen=True, - arbitrary_types_allowed=True, - ) - descriptors: List[SerializedDescriptor] = [] - nixl_metadata: str = "" - notification_key: str = "" - operation_kind: int = 0 - - def to_descriptors(self) -> Descriptor | list[Descriptor]: - """ - Deserializes the request descriptor into a `dynamo.connect.Descriptor` or list of `dynamo.connect.Descriptor` objects. - """ - if len(self.descriptors) == 0: - raise ValueError("Request descriptor must contain at least one serialized descriptor.") - if len(self.descriptors) == 1: - return self.descriptors[0].to_descriptor() - return [item.to_descriptor() for item in self.descriptors] - - @field_validator("operation_kind") - def validate_operation_kind(cls, v: int) -> int: - if v < 1 or v > 3: - raise TypeError("Argument `operation_kind` must be an integer value of `dynamo.connect.OperationKind`.") - return v - - -class WritableOperation(PassiveOperation): - """ - Operation which can be awaited until written to by a `WriteOperation` from a remote worker. - """ - - def __init__( - self, - connector: Connector, - local_descriptors: Descriptor | list[Descriptor], - ) -> None: - """ - Creates a new instance of `WritableOperation`, registers the operation and descriptors w/ NIXL, - and enables an RDMA write operation to occur. - - Parameters - ---------- - connector : Connector - Connector instance to use for the operation. - local_descriptors : Descriptor | list[Descriptor] - Descriptors to receive data from a remote worker. - - Raises - TypeError - When `local` is not a `dynamo.connect.Connector`. - TypeError - When `local_descriptors` is not a `dynamo.connect.Descriptor` or `list[dynamo.connect.Descriptor]`. - """ - super().__init__(connector, OperationKind.WRITE, local_descriptors) - logger.debug(f"Created {self.__repr__()}") - - def __del__(self) -> None: - super().__del__() - logger.debug(f"Deleted {self.__repr__()}") - - def __enter__(self) -> WritableOperation: - super().__enter__() - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - super().__exit__(exc_type, exc_value, traceback) - - def __repr__(self) -> str: - return super().__repr__() - - async def wait_for_completion(self) -> None: - """ - Blocks the caller asynchronously until the operation has completed. - """ - await super()._wait_for_completion_() - - -class WriteOperation(ActiveOperation): - """ - Awaitable write operation which initiates an RDMA write operation to a remote worker - which provided a `SerializedRequest` object from a `WritableOperation`. - """ - - def __init__( - self, - connector: Connector, - local_descriptors: Descriptor | list[Descriptor], - remote_request: SerializedRequest, - ) -> None: - """ - Creates a new instance of `WriteOperation`, registers `local_descriptors` with NIXL, - and begins an RDMA write operation which will transfer from `local_descriptors` to - remote target(s) described by `remote_request` - - Parameters - ---------- - connector : Connector - Connector instance to use for the operation. - local_descriptors : Descriptor | list[Descriptor] - Local descriptor(s) to send from, to the remote worker. - remote_request : SerializedRequest - Serialized request from the remote worker that describes the target(s) to send to. - - Raises - TypeError - When `connector` is not a `dynamo.connect.Connector`. - TypeError - When `remote_request` is not a `dynamo.connect.RequestDescriptor`. - ValueError - When `remote_request` is not of kind `WRITE`. - ValueError - When `remote_request.nixl_metadata` is not a non-empty `str`. - TypeError - When `local_descriptors` is not a `dynamo.connect.Descriptor` or `list[dynamo.connect.Descriptor]`. - """ - if not isinstance(connector, Connector): - raise TypeError("Argument `connector` must be `dynamo.connect.Connector`.") - if not isinstance(remote_request, SerializedRequest): - raise TypeError("Argument `remote_request` must be `dynamo.connect.RequestDescriptor`.") - if remote_request.operation_kind != OperationKind.WRITE.value: - raise ValueError("Argument `remote_request` must be of kind `WRITE`.") - - remote = Remote(connector, remote_request.nixl_metadata) - remote_descriptors = remote_request.to_descriptors() - - super().__init__(remote, OperationKind.WRITE, local_descriptors, remote_descriptors, remote_request.notification_key) - logger.debug(f"Created {self.__repr__()}") - - def __del__(self) -> None: - super().__del__() - logger.debug(f"Deleted {self.__repr__()}") - - def __enter__(self) -> WriteOperation: - super().__enter__() - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - super().__exit__(exc_type, exc_value, traceback) - - def __repr__(self) -> str: - return super().__repr__() - - def cancel(self) -> None: - """ - Cancels the operation. - No affect if the operation has already completed or errored, or has been cancelled. - """ - super()._cancel_() - - async def wait_for_completion(self) -> None: - """ - Blocks the caller asynchronously until the operation has completed. - """ - await super()._wait_for_completion_() diff --git a/examples/multimodal/utils/protocol.py b/examples/multimodal/utils/protocol.py index c6f2a70c59..f5083cc441 100644 --- a/examples/multimodal/utils/protocol.py +++ b/examples/multimodal/utils/protocol.py @@ -17,7 +17,6 @@ import json from typing import Any, List, Literal, Optional, Union -import connect import msgspec from pydantic import BaseModel, ConfigDict, field_validator from pydantic_core import core_schema @@ -27,6 +26,8 @@ from vllm.sampling_params import SamplingParams from vllm.sequence import PromptLogprobs, RequestMetrics +import dynamo.nixl_connect as connect + class Request(BaseModel): prompt: str @@ -127,7 +128,7 @@ class vLLMMultimodalRequest(vLLMGenerateRequest): model_config = ConfigDict(arbitrary_types_allowed=True) image_url: Optional[str] = None # image_features: Optional[List[List[List[float]]]] = None # Remove once have NIXL support - serialized_request: Optional[connect.SerializedRequest] = None + serialized_request: Optional[connect.RdmaMetadata] = None class EncodeRequest(BaseModel): @@ -138,7 +139,7 @@ class EncodeRequest(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) image_url: str request_id: str - serialized_request: Optional[connect.SerializedRequest] = None + serialized_request: Optional[connect.RdmaMetadata] = None class EncodeResponse(BaseModel): @@ -146,7 +147,7 @@ class EncodeResponse(BaseModel): request_id: str image_grid_thw: Optional[List[Any]] = None image_sizes: Optional[List[Any]] = None - serialized_request: Optional[connect.SerializedRequest] = None + serialized_request: Optional[connect.RdmaMetadata] = None image_features: List[List[List[float]]] # Remove once have NIXL support From 37c4680d43acfa912ce01ae856bc2b8a62b0a4ec Mon Sep 17 00:00:00 2001 From: Bhuvan Agrawal <11240550+bhuvan002@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:41:30 -0400 Subject: [PATCH 03/82] feat: add BaseLogitsProcessor core interface (#2613) Signed-off-by: Bhuvan Agrawal <11240550+bhuvan002@users.noreply.github.com> Signed-off-by: Jason Zhou --- .../src/dynamo/logits_processing/__init__.py | 13 +++++++ .../src/dynamo/logits_processing/base.py | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 lib/bindings/python/src/dynamo/logits_processing/__init__.py create mode 100644 lib/bindings/python/src/dynamo/logits_processing/base.py diff --git a/lib/bindings/python/src/dynamo/logits_processing/__init__.py b/lib/bindings/python/src/dynamo/logits_processing/__init__.py new file mode 100644 index 0000000000..1849385999 --- /dev/null +++ b/lib/bindings/python/src/dynamo/logits_processing/__init__.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Dynamo Logits Processing - Backend-agnostic logits processors. + +This module provides the BaseLogitsProcessor protocol that can be used +across different backend adapters (TRT-LLM, vLLM, SGLang). +""" + +from .base import BaseLogitsProcessor + +__all__ = ["BaseLogitsProcessor"] diff --git a/lib/bindings/python/src/dynamo/logits_processing/base.py b/lib/bindings/python/src/dynamo/logits_processing/base.py new file mode 100644 index 0000000000..5650479b0f --- /dev/null +++ b/lib/bindings/python/src/dynamo/logits_processing/base.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Base logits processor protocol for Dynamo. + +This module defines the core BaseLogitsProcessor interface that all +logits processors must implement. +""" + +from typing import Protocol, Sequence + +import torch + + +class BaseLogitsProcessor(Protocol): + """ + Protocol for logits processors in Dynamo. + + All logits processors must implement this interface to be compatible + with backend adapters (TRT-LLM, vLLM, SGLang). + """ + + def __call__( + self, + input_ids: Sequence[int], + logits: torch.Tensor, + ) -> torch.Tensor: + """ + Process the logits for the next token prediction. + + Args: + input_ids: The input token IDs generated so far. + logits: The raw logits for the next token. Shape: (vocab_size,) + + Returns: + A tensor with the same shape, dtype, and device as `logits`. + """ + ... From 5c991dcb4f8b45176e984a77ac93553eaf5b1dff Mon Sep 17 00:00:00 2001 From: Graham King Date: Fri, 22 Aug 2025 15:25:56 -0400 Subject: [PATCH 04/82] fix: Tests now pass with RUST_BACKTRACE set (#2647) Signed-off-by: Jason Zhou --- lib/llm/src/migration.rs | 14 ++++++++++++-- lib/llm/src/protocols/common/llm_backend.rs | 4 ++-- lib/runtime/src/protocols/annotated.rs | 6 +++--- lib/runtime/src/protocols/maybe_error.rs | 6 +++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/llm/src/migration.rs b/lib/llm/src/migration.rs index cb70550960..8ed3a24b28 100644 --- a/lib/llm/src/migration.rs +++ b/lib/llm/src/migration.rs @@ -100,10 +100,13 @@ impl RetryManager { if let Some(response) = response_stream.next().await { if let Some(err) = response.err() { const STREAM_ERR_MSG: &str = "Stream ended before generation completed"; - if format!("{:?}", err) == STREAM_ERR_MSG { + if err + .chain() + .any(|e| e.to_string().starts_with(STREAM_ERR_MSG)) + { tracing::warn!("Stream disconnected... recreating stream..."); if let Err(err) = self.new_stream().await { - tracing::warn!("Cannot recreate stream: {:?}", err); + tracing::warn!("Cannot recreate stream: {:#}", err); } else { continue; } @@ -462,6 +465,7 @@ mod tests { /// Expected behavior: All 10 responses should be received successfully. #[tokio::test] async fn test_retry_manager_no_migration() { + dynamo_runtime::logging::init(); let request = create_mock_request(10); let mock_engine = Arc::new(MockEngine::new(MockBehavior::Success, 10, 100)); let next_generate: ServerStreamingEngine> = @@ -493,6 +497,7 @@ mod tests { /// Expected behavior: All 10 responses should be received successfully after retry. #[tokio::test] async fn test_retry_manager_new_request_migration() { + dynamo_runtime::logging::init(); let request = create_mock_request(10); let mock_engine = Arc::new(MockEngine::new(MockBehavior::FailThenSuccess, 10, 100)); let next_generate: ServerStreamingEngine> = @@ -524,6 +529,8 @@ mod tests { /// Expected behavior: 5 responses from first stream + 5 responses from retry stream = 10 total. #[tokio::test] async fn test_retry_manager_ongoing_request_migration() { + dynamo_runtime::logging::init(); + let request = create_mock_request(10); let mock_engine = Arc::new(MockEngine::new( MockBehavior::MidStreamFail { fail_after: 5 }, @@ -560,6 +567,7 @@ mod tests { /// Expected behavior: Should receive an error after all retries are exhausted, with the original error. #[tokio::test] async fn test_retry_manager_new_request_migration_indefinite_failure() { + dynamo_runtime::logging::init(); let request = create_mock_request(0); let mock_engine = Arc::new(MockEngine::new(MockBehavior::AlwaysFail, 0, 100)); let next_generate: ServerStreamingEngine> = @@ -580,6 +588,7 @@ mod tests { /// Expected behavior: Should receive some responses from first stream, then error after retries exhausted. #[tokio::test] async fn test_retry_manager_ongoing_request_migration_indefinite_failure() { + dynamo_runtime::logging::init(); let request = create_mock_request(10); let mock_engine = Arc::new(MockEngine::new( MockBehavior::MidStreamFailAlways { fail_after: 3 }, @@ -627,6 +636,7 @@ mod tests { /// Expected behavior: Should receive some responses from first stream, then error after retries exhausted. #[tokio::test] async fn test_retry_manager_ongoing_request_migration_indefinite_failure_stream_error() { + dynamo_runtime::logging::init(); let request = create_mock_request(10); let mock_engine = Arc::new(MockEngine::new( MockBehavior::MidStreamFailAlwaysStreamError { fail_after: 3 }, diff --git a/lib/llm/src/protocols/common/llm_backend.rs b/lib/llm/src/protocols/common/llm_backend.rs index bde2e8f376..1c00e837a6 100644 --- a/lib/llm/src/protocols/common/llm_backend.rs +++ b/lib/llm/src/protocols/common/llm_backend.rs @@ -157,9 +157,9 @@ impl MaybeError for LLMEngineOutput { LLMEngineOutput::error(format!("{:?}", err)) } - fn err(&self) -> Option> { + fn err(&self) -> Option { if let Some(FinishReason::Error(err_msg)) = &self.finish_reason { - Some(anyhow::Error::msg(err_msg.clone()).into()) + Some(anyhow::Error::msg(err_msg.clone())) } else { None } diff --git a/lib/runtime/src/protocols/annotated.rs b/lib/runtime/src/protocols/annotated.rs index 204a897b18..33ffa976f9 100644 --- a/lib/runtime/src/protocols/annotated.rs +++ b/lib/runtime/src/protocols/annotated.rs @@ -143,14 +143,14 @@ where Annotated::from_error(format!("{:?}", err)) } - fn err(&self) -> Option> { + fn err(&self) -> Option { if self.is_error() { if let Some(comment) = &self.comment { if !comment.is_empty() { - return Some(anyhow::Error::msg(comment.join("; ")).into()); + return Some(anyhow::Error::msg(comment.join("; "))); } } - Some(anyhow::Error::msg("unknown error").into()) + Some(anyhow::Error::msg("unknown error")) } else { None } diff --git a/lib/runtime/src/protocols/maybe_error.rs b/lib/runtime/src/protocols/maybe_error.rs index 068fbadc60..84339e3587 100644 --- a/lib/runtime/src/protocols/maybe_error.rs +++ b/lib/runtime/src/protocols/maybe_error.rs @@ -20,7 +20,7 @@ pub trait MaybeError { fn from_err(err: Box) -> Self; /// Construct into an error instance. - fn err(&self) -> Option>; + fn err(&self) -> Option; /// Check if the current instance represents a success. fn is_ok(&self) -> bool { @@ -46,8 +46,8 @@ mod tests { message: err.to_string(), } } - fn err(&self) -> Option> { - Some(anyhow::Error::msg(self.message.clone()).into()) + fn err(&self) -> Option { + Some(anyhow::Error::msg(self.message.clone())) } } From 0767cc1bff65e3788c9192b8d5644804f4ef56a7 Mon Sep 17 00:00:00 2001 From: Kris Hung Date: Fri, 22 Aug 2025 12:28:52 -0700 Subject: [PATCH 05/82] docs: Update supported model in readme for multimodal (#2651) Signed-off-by: Jason Zhou --- examples/multimodal/README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/multimodal/README.md b/examples/multimodal/README.md index 7637ca4db1..6b7963cecd 100644 --- a/examples/multimodal/README.md +++ b/examples/multimodal/README.md @@ -59,14 +59,12 @@ flowchart LR pd_worker --> encode_worker ``` +***Note*** Only the LLaVA 1.5 7B model is supported. Qwen2.5-VL and Phi3V support will be added in the future. + ```bash cd $DYNAMO_HOME/examples/multimodal # Serve a LLaVA 1.5 7B model: bash launch/agg.sh --model llava-hf/llava-1.5-7b-hf -# Serve a Qwen2.5-VL model: -# bash launch/agg.sh --model Qwen/Qwen2.5-VL-7B-Instruct -# Serve a Phi3V model: -# bash launch/agg.sh --model microsoft/Phi-3.5-vision-instruct ``` ### Client @@ -100,8 +98,6 @@ curl http://localhost:8080/v1/chat/completions \ }' ``` -If serving the example Qwen model, replace `"llava-hf/llava-1.5-7b-hf"` in the `"model"` field with `"Qwen/Qwen2.5-VL-7B-Instruct"`. If serving the example Phi3V model, replace `"llava-hf/llava-1.5-7b-hf"` in the `"model"` field with `"microsoft/Phi-3.5-vision-instruct"`. - You should see a response similar to this: ```json {"id": "c37b946e-9e58-4d54-88c8-2dbd92c47b0c", "object": "chat.completion", "created": 1747725277, "model": "llava-hf/llava-1.5-7b-hf", "choices": [{"index": 0, "message": {"role": "assistant", "content": " In the image, there is a city bus parked on a street, with a street sign nearby on the right side. The bus appears to be stopped out of service. The setting is in a foggy city, giving it a slightly moody atmosphere."}, "finish_reason": "stop"}]} From 3ef8e849ca303af7ba7b24fa74f0a86777d2f19c Mon Sep 17 00:00:00 2001 From: Ziqi Fan Date: Fri, 22 Aug 2025 12:58:05 -0700 Subject: [PATCH 06/82] feat: enable dynamo metrics on KVBM (#2626) Signed-off-by: Jason Zhou --- .../grafana-kvbm-dashboard.json | 234 ++++++++++++++++++ deploy/metrics/prometheus.yml | 12 + docs/guides/run_kvbm_in_vllm.md | 18 ++ lib/bindings/python/Cargo.lock | 1 + lib/bindings/python/Cargo.toml | 1 + .../block_manager/vllm/connector/leader.rs | 20 +- .../vllm/connector/leader/recorder.rs | 11 +- .../vllm/connector/leader/slot.rs | 21 +- .../block_manager/vllm/connector/worker.rs | 11 + .../llm/vllm_integration/connector_leader.py | 4 + .../llm/vllm_integration/connector_worker.py | 5 + .../llm/vllm_integration/kv_cache_utils.py | 28 +++ lib/llm/src/block_manager.rs | 1 + lib/llm/src/block_manager/metrics_kvbm.rs | 30 +++ lib/runtime/src/metrics/prometheus_names.rs | 9 + 15 files changed, 396 insertions(+), 10 deletions(-) create mode 100644 deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json create mode 100644 lib/llm/src/block_manager/metrics_kvbm.rs diff --git a/deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json b/deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json new file mode 100644 index 0000000000..56ed93c779 --- /dev/null +++ b/deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json @@ -0,0 +1,234 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "All KVBM related metrics", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 4, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_save_kv_layer_requests{dynamo_namespace=\"kvbm_connector_worker\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "KVBM Worker: save kv layer requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_offload_requests{dynamo_namespace=\"kvbm_connector_leader\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "KVBM Leader: offload requests", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "auto", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "KVBM Dashboard", + "uid": "3f679257-70a5-402c-92b4-05382337b548", + "version": 7 + } diff --git a/deploy/metrics/prometheus.yml b/deploy/metrics/prometheus.yml index e7192b484f..2a41b42869 100644 --- a/deploy/metrics/prometheus.yml +++ b/deploy/metrics/prometheus.yml @@ -58,6 +58,18 @@ scrape_configs: # - targets: ['localhost:9091'] # metrics aggregation service on host - targets: ['host.docker.internal:9091'] # metrics aggregation service on host + # KVBM leader related metrics + - job_name: 'kvbm-leader-metrics' + scrape_interval: 2s + static_configs: + - targets: ['host.docker.internal:6881'] + + # KVBM worker related metrics + - job_name: 'kvbm-worker-metrics' + scrape_interval: 2s + static_configs: + - targets: ['host.docker.internal:6880'] + # Uncomment to see its own Prometheus metrics # - job_name: 'prometheus' # scrape_interval: 5s diff --git a/docs/guides/run_kvbm_in_vllm.md b/docs/guides/run_kvbm_in_vllm.md index d3211f72e7..ed67ac84f0 100644 --- a/docs/guides/run_kvbm_in_vllm.md +++ b/docs/guides/run_kvbm_in_vllm.md @@ -59,3 +59,21 @@ curl localhost:8000/v1/chat/completions -H "Content-Type: application/json" "max_tokens": 30 }' ``` + +## Enable and View KVBM Metrics + +Follow below steps to enable metrics collection and view via Grafana dashboard: +```bash +# Start the basic services (etcd & natsd), along with Prometheus and Grafana +docker compose -f deploy/docker-compose.yml --profile metrics up -d + +# start vllm with DYN_SYSTEM_ENABLED set to true and DYN_SYSTEM_PORT port to 6880. +# NOTE: Make sure port 6880 (for KVBM worker metrics) and port 6881 (for KVBM leader metrics) are available. +DYN_SYSTEM_ENABLED=true DYN_SYSTEM_PORT=6880 vllm serve --kv-transfer-config '{"kv_connector":"DynamoConnector","kv_role":"kv_both", "kv_connector_module_path": "dynamo.llm.vllm_integration.connector"}' deepseek-ai/DeepSeek-R1-Distill-Llama-8B + +# optional if firewall blocks KVBM metrics ports to send prometheus metrics +sudo ufw allow 6880/tcp +sudo ufw allow 6881/tcp +``` + +View grafana metrics via http://localhost:3001 (default login: dynamo/dynamo) and look for KVBM Dashboard diff --git a/lib/bindings/python/Cargo.lock b/lib/bindings/python/Cargo.lock index 337c8a3845..105e3e88a3 100644 --- a/lib/bindings/python/Cargo.lock +++ b/lib/bindings/python/Cargo.lock @@ -1333,6 +1333,7 @@ dependencies = [ "either", "futures", "once_cell", + "prometheus", "pyo3", "pyo3-async-runtimes", "pythonize", diff --git a/lib/bindings/python/Cargo.toml b/lib/bindings/python/Cargo.toml index 81d5ee00cc..6e03383489 100644 --- a/lib/bindings/python/Cargo.toml +++ b/lib/bindings/python/Cargo.toml @@ -80,6 +80,7 @@ pythonize = "0.23" dlpark = { version = "0.5", features = ["pyo3", "half"], optional = true } cudarc = { version = "0.16.2", features = ["cuda-12020"], optional = true } +prometheus = "0.14.0" [dev-dependencies] diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs index 7aab84ce28..d6b787de74 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs @@ -5,6 +5,7 @@ pub mod recorder; pub mod slot; use super::*; +use dynamo_llm::block_manager::metrics_kvbm::KvbmMetrics; use dynamo_runtime::DistributedRuntime; use slot::{ConnectorSlotManager, SlotError, SlotManager, SlotState}; @@ -14,6 +15,7 @@ use crate::llm::block_manager::{ vllm::KvbmRequest, VllmBlockManager, }; use crate::DistributedRuntime as PyDistributedRuntime; +use dynamo_runtime::metrics::prometheus_names::kvbm_connector; use dynamo_llm::block_manager::{ block::{ @@ -25,10 +27,7 @@ use dynamo_llm::block_manager::{ }; use dynamo_llm::tokens::{SaltHash, TokenBlockSequence, Tokens}; -use std::{ - collections::HashSet, - sync::{Arc, Mutex}, -}; +use std::{collections::HashSet, sync::Mutex}; use tokio; use tokio::sync::mpsc; @@ -104,8 +103,19 @@ impl KvConnectorLeader { // if we need a drt, get it from here let drt = drt.inner().clone(); + let ns = drt + .namespace(kvbm_connector::KVBM_CONNECTOR_LEADER) + .unwrap(); + + let kvbm_metrics = KvbmMetrics::new(&ns); + Self { - slot_manager: ConnectorSlotManager::new(block_manager.clone(), leader, drt.clone()), + slot_manager: ConnectorSlotManager::new( + block_manager.clone(), + leader, + drt.clone(), + kvbm_metrics, + ), block_size, inflight_requests: HashSet::new(), onboarding_slots: HashSet::new(), diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs index 2c0accdd44..6506851c2a 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs @@ -109,6 +109,10 @@ impl KvConnectorLeaderRecorder { let output_path = "/tmp/records.jsonl"; tracing::info!("recording events to {}", output_path); + let ns = drt.namespace("kvbm_connector_leader").unwrap(); + + let kvbm_metrics = KvbmMetrics::new(&ns); + let recorder = drt .runtime() .primary() @@ -116,7 +120,12 @@ impl KvConnectorLeaderRecorder { .unwrap(); let connector_leader = KvConnectorLeader { - slot_manager: ConnectorSlotManager::new(block_manager.clone(), leader, drt.clone()), + slot_manager: ConnectorSlotManager::new( + block_manager.clone(), + leader, + drt.clone(), + kvbm_metrics, + ), block_size, inflight_requests: HashSet::new(), onboarding_slots: HashSet::new(), diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs index a4d5fa9139..74c729e11a 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use std::any::Any; +use std::{any::Any, sync::Arc}; use dynamo_llm::{ block_manager::{ @@ -179,6 +179,7 @@ impl ConnectorSlotManager { block_manager: VllmBlockManager, leader: Arc, drt: DistributedRuntime, + kvbm_metrics: KvbmMetrics, ) -> Self { tracing::debug!( "creating slot manager with block size: {}", @@ -190,11 +191,14 @@ impl ConnectorSlotManager { let mut xfer_engine = LocalTransferEngine::new(block_manager.clone(), leader, xfer_rx); let primary_token = drt.primary_token(); let runtime_primary = drt.runtime().primary(); + let drt_for_task = drt; let xfer_engine_task = CriticalTaskExecutionHandle::new_with_runtime( |cancellation_token| async move { - xfer_engine.execute(cancellation_token, drt_for_task).await + xfer_engine + .execute(cancellation_token, drt_for_task, kvbm_metrics.clone()) + .await }, primary_token, "LocalTransferEngine", @@ -1027,6 +1031,7 @@ impl LocalTransferEngine { &mut self, cancellation_token: CancellationToken, drt: DistributedRuntime, + kvbm_metrics: KvbmMetrics, ) -> anyhow::Result<()> { let (onboard_tx, mut onboard_rx) = mpsc::unbounded_channel(); let (offload_tx, mut offload_rx) = mpsc::unbounded_channel(); @@ -1062,8 +1067,13 @@ impl LocalTransferEngine { tracing::debug!("LocalOffloadTask: received cancellation signal"); break; } - if let Err(e) = - process_offload_request(req, &block_manager_offload, &leader_offload).await + if let Err(e) = process_offload_request( + req, + &block_manager_offload, + &leader_offload, + kvbm_metrics.clone(), + ) + .await { tracing::error!("LocalOffloadTask: error processing request: {:?}", e); } @@ -1132,7 +1142,10 @@ async fn process_offload_request( offload_req: LocalOffloadRequest, block_manager: &VllmBlockManager, leader: &Arc, + kvbm_metrics: KvbmMetrics, ) -> anyhow::Result<()> { + kvbm_metrics.offload_requests.inc(); + let request_id = &offload_req.request_id; let operation_id = &offload_req.operation_id; diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs index 1c941686df..eccb80b473 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs @@ -5,6 +5,7 @@ use dynamo_llm::block_manager::connector::protocol::TransferType; use dynamo_llm::block_manager::connector::scheduler::{ Scheduler, TransferSchedulerClient, WorkerSchedulerClient, }; +use dynamo_llm::block_manager::metrics_kvbm::KvbmMetrics; use std::collections::HashSet; use std::sync::{Arc, OnceLock}; @@ -15,6 +16,7 @@ use crate::{ llm::block_manager::distributed::VllmTensor, to_pyerr, DistributedRuntime as PyDistributedRuntime, }; +use dynamo_runtime::metrics::prometheus_names::kvbm_connector; use anyhow; use dynamo_llm::block_manager::distributed::{KvbmWorker, KvbmWorkerConfig}; @@ -68,6 +70,8 @@ pub struct KvConnectorWorker { /// cuda events created by the python side layer_events: Vec, + + kvbm_metrics: KvbmMetrics, } impl KvConnectorWorker { @@ -88,6 +92,11 @@ impl KvConnectorWorker { )? .detach(); + let kvbm_metrics = KvbmMetrics::new( + &drt.namespace(kvbm_connector::KVBM_CONNECTOR_WORKER) + .unwrap(), + ); + tracing::info!( "KvConnectorWorker initialized with worker_id: {}", vllm_worker_id @@ -106,6 +115,7 @@ impl KvConnectorWorker { layers_complete: 0, kv_cache_layers: Vec::new(), layer_events: Vec::new(), + kvbm_metrics, }) } } @@ -255,6 +265,7 @@ impl Worker for KvConnectorWorker { /// Trigger layer-wise completion signals. /// Trigger block-wise completion signals afer last layer. fn save_kv_layer(&mut self, _layer_name: String) -> anyhow::Result<()> { + self.kvbm_metrics.save_kv_layer_requests.inc(); self.layers_complete += 1; if self.layers_complete == self.kv_cache_layers.len() { let offloading_operations = std::mem::take(&mut self.offloading_operations); diff --git a/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_leader.py b/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_leader.py index eeb62a8c30..e730acafc0 100644 --- a/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_leader.py +++ b/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_leader.py @@ -30,6 +30,9 @@ # from dynamo.llm.vllm_integration.rust import SchedulerOutput as RustSchedulerOutput from dynamo.llm import BlockManager, KvbmLeader +from dynamo.llm.vllm_integration.kv_cache_utils import ( + find_and_set_available_port_from_env, +) from dynamo.llm.vllm_integration.rust import KvbmRequest from dynamo.llm.vllm_integration.rust import KvConnectorLeader as RustKvConnectorLeader from dynamo.llm.vllm_integration.rust import SchedulerOutput as RustSchedulerOutput @@ -54,6 +57,7 @@ class KvConnectorLeader: def __init__(self, vllm_config: "VllmConfig", engine_id: str, **kwargs): drt = kwargs.get("drt", None) if drt is None: + find_and_set_available_port_from_env("DYN_SYSTEM_PORT") self.drt = DistributedRuntime.detached() else: self.drt = drt diff --git a/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_worker.py b/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_worker.py index 411cdd3f98..ce0a85093d 100644 --- a/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_worker.py +++ b/lib/bindings/python/src/dynamo/llm/vllm_integration/connector_worker.py @@ -28,6 +28,9 @@ # KvConnectorWorker as RustKvConnectorWorker, # ) +from dynamo.llm.vllm_integration.kv_cache_utils import ( + find_and_set_available_port_from_env, +) from dynamo.llm.vllm_integration.rust import KvConnectorWorker as RustKvConnectorWorker from dynamo.runtime import DistributedRuntime @@ -42,6 +45,8 @@ class KvConnectorWorker: def __init__(self, vllm_config: "VllmConfig", engine_id: str, **kwargs): drt = kwargs.get("drt", None) if drt is None: + # this is needed to avoid metrics port conflict with KVBM leader side DRT if metrics is enabled + find_and_set_available_port_from_env("DYN_SYSTEM_PORT") self.drt = DistributedRuntime.detached() else: self.drt = drt diff --git a/lib/bindings/python/src/dynamo/llm/vllm_integration/kv_cache_utils.py b/lib/bindings/python/src/dynamo/llm/vllm_integration/kv_cache_utils.py index 5acb8b33f9..f8b3747c07 100644 --- a/lib/bindings/python/src/dynamo/llm/vllm_integration/kv_cache_utils.py +++ b/lib/bindings/python/src/dynamo/llm/vllm_integration/kv_cache_utils.py @@ -7,6 +7,8 @@ from __future__ import annotations +import os +import socket from typing import List from vllm.v1.core.kv_cache_manager import KVCacheBlocks @@ -86,3 +88,29 @@ def convert_kv_cache_blocks(blocks: KVCacheBlocks) -> BlockStates: for block in blocks.blocks: states.push_back(convert_kv_cache_block(block)) return states + + +# TODO(keiven|ziqi): Auto port selection to be done in Rust +def find_and_set_available_port_from_env(env_var="DYN_SYSTEM_PORT"): + """ + Find an available port from the environment variable. + """ + port = int(os.environ.get(env_var, "0")) + if port == 0: + # No port specified, let system pick + pass + while True: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + # Port is available + s.bind(("127.0.0.1", port)) + s.close() + os.environ[env_var] = str(port) + print(f"Port {port} is available, setting env var {env_var} to {port}") + break + except OSError: + # Port is in use, try next + port += 1 + s.close() + except Exception as e: + raise RuntimeError(f"Error finding available port: {e}") diff --git a/lib/llm/src/block_manager.rs b/lib/llm/src/block_manager.rs index 0ff0abf7c6..9f1f2566d4 100644 --- a/lib/llm/src/block_manager.rs +++ b/lib/llm/src/block_manager.rs @@ -28,6 +28,7 @@ pub mod distributed; pub mod events; pub mod layout; pub mod metrics; +pub mod metrics_kvbm; pub mod offload; pub mod pool; pub mod storage; diff --git a/lib/llm/src/block_manager/metrics_kvbm.rs b/lib/llm/src/block_manager/metrics_kvbm.rs new file mode 100644 index 0000000000..0b8e58d3f2 --- /dev/null +++ b/lib/llm/src/block_manager/metrics_kvbm.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use dynamo_runtime::metrics::MetricsRegistry; +use prometheus::IntCounter; + +#[derive(Clone, Debug)] +pub struct KvbmMetrics { + pub offload_requests: IntCounter, + pub save_kv_layer_requests: IntCounter, +} + +impl KvbmMetrics { + pub fn new(mr: &dyn MetricsRegistry) -> Self { + let offload_requests = mr + .create_intcounter("offload_requests", "The number of offload requests", &[]) + .unwrap(); + let save_kv_layer_requests = mr + .create_intcounter( + "save_kv_layer_requests", + "The number of save kv layer requests", + &[], + ) + .unwrap(); + Self { + offload_requests, + save_kv_layer_requests, + } + } +} diff --git a/lib/runtime/src/metrics/prometheus_names.rs b/lib/runtime/src/metrics/prometheus_names.rs index 63ec74d703..87bbac924f 100644 --- a/lib/runtime/src/metrics/prometheus_names.rs +++ b/lib/runtime/src/metrics/prometheus_names.rs @@ -132,3 +132,12 @@ pub mod work_handler { /// Time spent processing requests by work handler (histogram) pub const REQUEST_DURATION_SECONDS: &str = "request_duration_seconds"; } + +/// KVBM connector +pub mod kvbm_connector { + /// KVBM connector leader + pub const KVBM_CONNECTOR_LEADER: &str = "kvbm_connector_leader"; + + /// KVBM connector worker + pub const KVBM_CONNECTOR_WORKER: &str = "kvbm_connector_worker"; +} From 216f608f1389e29c32c484de385e963b3d901b24 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Fri, 22 Aug 2025 13:29:18 -0700 Subject: [PATCH 07/82] feat: [vLLM] implement cli args for tool and reasoning parsers (#2619) Signed-off-by: Jason Zhou --- .../backends/vllm/src/dynamo/vllm/args.py | 20 +++++- .../backends/vllm/src/dynamo/vllm/main.py | 2 + lib/bindings/python/rust/llm/local_model.rs | 20 ++++++ lib/llm/src/discovery/model_manager.rs | 12 ++++ lib/llm/src/http/service/openai.rs | 70 ++++++++++++------- lib/llm/src/local_model.rs | 2 + lib/llm/src/local_model/runtime_config.rs | 4 ++ lib/llm/src/preprocessor.rs | 1 - lib/llm/src/protocols/openai.rs | 16 +++++ .../openai/chat_completions/aggregator.rs | 28 +++++--- .../openai/completions/aggregator.rs | 20 ++++-- lib/llm/tests/aggregators.rs | 41 +++++++---- lib/parsers/src/tool_calling/tools.rs | 5 ++ 13 files changed, 183 insertions(+), 58 deletions(-) diff --git a/components/backends/vllm/src/dynamo/vllm/args.py b/components/backends/vllm/src/dynamo/vllm/args.py index d0f0c9b341..381fcb38fc 100644 --- a/components/backends/vllm/src/dynamo/vllm/args.py +++ b/components/backends/vllm/src/dynamo/vllm/args.py @@ -58,6 +58,10 @@ class Config: # Connector list from CLI connector_list: Optional[list] = None + # tool and reasoning parser info + tool_call_parser: Optional[str] = None + reasoning_parser: Optional[str] = None + def parse_args() -> Config: parser = FlexibleArgumentParser( @@ -102,6 +106,19 @@ def parse_args() -> Config: help="List of connectors to use in order (e.g., --connector nixl lmcache). " "Options: nixl, lmcache, kvbm, null, none. Default: nixl. Order will be preserved in MultiConnector.", ) + # To avoid name conflicts with different backends, adoped prefix "dyn-" for dynamo specific args + parser.add_argument( + "--dyn-tool-call-parser", + type=str, + default=None, + help="Tool call parser name for the model. Available options: 'hermes', 'nemotron_deci', 'llama3_json', 'mistral', 'phi4'.", + ) + parser.add_argument( + "--dyn-reasoning-parser", + type=str, + default=None, + help="Reasoning parser name for the model.", + ) parser = AsyncEngineArgs.add_cli_args(parser) args = parser.parse_args() @@ -151,7 +168,8 @@ def parse_args() -> Config: config.port_range = DynamoPortRange( min=args.dynamo_port_min, max=args.dynamo_port_max ) - + config.tool_call_parser = args.dyn_tool_call_parser + config.reasoning_parser = args.dyn_reasoning_parser # Check for conflicting flags has_kv_transfer_config = ( hasattr(engine_args, "kv_transfer_config") diff --git a/components/backends/vllm/src/dynamo/vllm/main.py b/components/backends/vllm/src/dynamo/vllm/main.py index 7e0486c915..5be1830c99 100644 --- a/components/backends/vllm/src/dynamo/vllm/main.py +++ b/components/backends/vllm/src/dynamo/vllm/main.py @@ -234,6 +234,8 @@ async def init(runtime: DistributedRuntime, config: Config): runtime_config.total_kv_blocks = runtime_values["num_gpu_blocks"] runtime_config.max_num_seqs = runtime_values["max_num_seqs"] runtime_config.max_num_batched_tokens = runtime_values["max_num_batched_tokens"] + runtime_config.tool_call_parser = config.tool_call_parser + runtime_config.reasoning_parser = config.reasoning_parser await register_llm( ModelType.Backend, diff --git a/lib/bindings/python/rust/llm/local_model.rs b/lib/bindings/python/rust/llm/local_model.rs index 2fdc1a153b..fc1f365906 100644 --- a/lib/bindings/python/rust/llm/local_model.rs +++ b/lib/bindings/python/rust/llm/local_model.rs @@ -34,6 +34,16 @@ impl ModelRuntimeConfig { self.inner.max_num_batched_tokens = Some(max_num_batched_tokens); } + #[setter] + fn set_tool_call_parser(&mut self, tool_call_parser: Option) { + self.inner.tool_call_parser = tool_call_parser; + } + + #[setter] + fn set_reasoning_parser(&mut self, reasoning_parser: Option) { + self.inner.reasoning_parser = reasoning_parser; + } + fn set_engine_specific(&mut self, key: &str, value: String) -> PyResult<()> { let value: serde_json::Value = serde_json::from_str(&value).map_err(to_pyerr)?; self.inner @@ -57,6 +67,16 @@ impl ModelRuntimeConfig { self.inner.max_num_batched_tokens } + #[getter] + fn tool_call_parser(&self) -> Option { + self.inner.tool_call_parser.clone() + } + + #[getter] + fn reasoning_parser(&self) -> Option { + self.inner.reasoning_parser.clone() + } + #[getter] fn runtime_data(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); diff --git a/lib/llm/src/discovery/model_manager.rs b/lib/llm/src/discovery/model_manager.rs index b934a75ccc..95e05baca0 100644 --- a/lib/llm/src/discovery/model_manager.rs +++ b/lib/llm/src/discovery/model_manager.rs @@ -246,6 +246,18 @@ impl ModelManager { .insert(model_name.to_string(), new_kv_chooser.clone()); Ok(new_kv_chooser) } + + pub fn get_model_tool_call_parser(&self, model: &str) -> Option { + match self.entries.lock() { + Ok(entries) => entries + .values() + .find(|entry| entry.name == model) + .and_then(|entry| entry.runtime_config.as_ref()) + .and_then(|config| config.tool_call_parser.clone()) + .map(|parser| parser.to_string()), + Err(_) => None, + } + } } pub struct ModelEngines { diff --git a/lib/llm/src/http/service/openai.rs b/lib/llm/src/http/service/openai.rs index ee3691f09f..8a2c9c53a0 100644 --- a/lib/llm/src/http/service/openai.rs +++ b/lib/llm/src/http/service/openai.rs @@ -37,6 +37,7 @@ use crate::protocols::openai::{ completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, embeddings::{NvCreateEmbeddingRequest, NvCreateEmbeddingResponse}, responses::{NvCreateResponse, NvResponse}, + ParsingOptions, }; use crate::request_template::RequestTemplate; use crate::types::Annotated; @@ -194,6 +195,13 @@ fn get_or_create_request_id(primary: Option<&str>, headers: &HeaderMap) -> Strin uuid.to_string() } +fn get_parsing_options(state: &Arc, model: &str) -> ParsingOptions { + let tool_call_parser = state.manager().get_model_tool_call_parser(model); + let reasoning_parser = None; // TODO: Implement reasoning parser + + ParsingOptions::new(tool_call_parser, reasoning_parser) +} + /// OpenAI Completions Request Handler /// /// This method will handle the incoming request for the `/v1/completions endpoint`. The endpoint is a "source" @@ -267,6 +275,8 @@ async fn completions( .get_completions_engine(model) .map_err(|_| ErrorMessage::model_not_found())?; + let parsing_options = get_parsing_options(&state, model); + let mut inflight_guard = state .metrics_clone() @@ -325,7 +335,7 @@ async fn completions( process_metrics_only(response, &mut response_collector); }); - let response = NvCreateCompletionResponse::from_annotated_stream(stream) + let response = NvCreateCompletionResponse::from_annotated_stream(stream, parsing_options) .await .map_err(|e| { tracing::error!( @@ -494,6 +504,8 @@ async fn chat_completions( .get_chat_completions_engine(model) .map_err(|_| ErrorMessage::model_not_found())?; + let parsing_options = get_parsing_options(&state, model); + let mut inflight_guard = state .metrics_clone() @@ -553,19 +565,20 @@ async fn chat_completions( process_metrics_only(response, &mut response_collector); }); - let response = NvCreateChatCompletionResponse::from_annotated_stream(stream) - .await - .map_err(|e| { - tracing::error!( - request_id, - "Failed to fold chat completions stream for: {:?}", - e - ); - ErrorMessage::internal_server_error(&format!( - "Failed to fold chat completions stream: {}", - e - )) - })?; + let response = + NvCreateChatCompletionResponse::from_annotated_stream(stream, parsing_options.clone()) + .await + .map_err(|e| { + tracing::error!( + request_id, + "Failed to fold chat completions stream for: {:?}", + e + ); + ErrorMessage::internal_server_error(&format!( + "Failed to fold chat completions stream: {}", + e + )) + })?; inflight_guard.mark_ok(); Ok(Json(response).into_response()) @@ -726,6 +739,8 @@ async fn responses( .get_chat_completions_engine(model) .map_err(|_| ErrorMessage::model_not_found())?; + let parsing_options = get_parsing_options(&state, model); + let mut inflight_guard = state .metrics_clone() @@ -742,19 +757,20 @@ async fn responses( .map_err(|e| ErrorMessage::from_anyhow(e, "Failed to generate completions"))?; // TODO: handle streaming, currently just unary - let response = NvCreateChatCompletionResponse::from_annotated_stream(stream) - .await - .map_err(|e| { - tracing::error!( - request_id, - "Failed to fold chat completions stream for: {:?}", - e - ); - ErrorMessage::internal_server_error(&format!( - "Failed to fold chat completions stream: {}", - e - )) - })?; + let response = + NvCreateChatCompletionResponse::from_annotated_stream(stream, parsing_options.clone()) + .await + .map_err(|e| { + tracing::error!( + request_id, + "Failed to fold chat completions stream for: {:?}", + e + ); + ErrorMessage::internal_server_error(&format!( + "Failed to fold chat completions stream: {}", + e + )) + })?; // Convert NvCreateChatCompletionResponse --> NvResponse let response: NvResponse = response.try_into().map_err(|e| { diff --git a/lib/llm/src/local_model.rs b/lib/llm/src/local_model.rs index 72708070e8..ab1a0c5b91 100644 --- a/lib/llm/src/local_model.rs +++ b/lib/llm/src/local_model.rs @@ -202,6 +202,7 @@ impl LocalModelBuilder { ); card.migration_limit = self.migration_limit; card.user_data = self.user_data.take(); + return Ok(LocalModel { card, full_path: PathBuf::new(), @@ -392,6 +393,7 @@ impl LocalModel { let kvstore: Box = Box::new(EtcdStorage::new(etcd_client.clone())); let card_store = Arc::new(KeyValueStoreManager::new(kvstore)); let key = self.card.slug().to_string(); + card_store .publish(model_card::ROOT_PATH, None, &key, &mut self.card) .await?; diff --git a/lib/llm/src/local_model/runtime_config.rs b/lib/llm/src/local_model/runtime_config.rs index 4421ff4022..8c5a6a434f 100644 --- a/lib/llm/src/local_model/runtime_config.rs +++ b/lib/llm/src/local_model/runtime_config.rs @@ -13,6 +13,10 @@ pub struct ModelRuntimeConfig { pub max_num_batched_tokens: Option, + pub tool_call_parser: Option, + + pub reasoning_parser: Option, + /// Mapping of engine-specific runtime configs #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub runtime_data: HashMap, diff --git a/lib/llm/src/preprocessor.rs b/lib/llm/src/preprocessor.rs index 917fcf0c50..f600d08c24 100644 --- a/lib/llm/src/preprocessor.rs +++ b/lib/llm/src/preprocessor.rs @@ -101,7 +101,6 @@ impl OpenAIPreprocessor { let mdcsum = mdc.mdcsum(); let formatter = PromptFormatter::from_mdc(mdc.clone()).await?; let PromptFormatter::OAI(formatter) = formatter; - let tokenizer = match &mdc.tokenizer { Some(TokenizerKind::HfTokenizerJson(file)) => HuggingFaceTokenizer::from_file(file)?, Some(TokenizerKind::GGUF(tokenizer)) => { diff --git a/lib/llm/src/protocols/openai.rs b/lib/llm/src/protocols/openai.rs index 7c3166dc4c..668d8e6933 100644 --- a/lib/llm/src/protocols/openai.rs +++ b/lib/llm/src/protocols/openai.rs @@ -193,3 +193,19 @@ pub trait DeltaGeneratorExt: /// Gets the current prompt token count (Input Sequence Length). fn get_isl(&self) -> Option; } + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct ParsingOptions { + pub tool_call_parser: Option, + + pub reasoning_parser: Option, +} + +impl ParsingOptions { + pub fn new(tool_call_parser: Option, reasoning_parser: Option) -> Self { + Self { + tool_call_parser, + reasoning_parser, + } + } +} diff --git a/lib/llm/src/protocols/openai/chat_completions/aggregator.rs b/lib/llm/src/protocols/openai/chat_completions/aggregator.rs index ed15b7d69e..a99b3e1dda 100644 --- a/lib/llm/src/protocols/openai/chat_completions/aggregator.rs +++ b/lib/llm/src/protocols/openai/chat_completions/aggregator.rs @@ -19,7 +19,9 @@ use std::collections::HashMap; use super::{NvCreateChatCompletionResponse, NvCreateChatCompletionStreamResponse}; use crate::protocols::{ codec::{Message, SseCodecError}, - convert_sse_stream, Annotated, + convert_sse_stream, + openai::ParsingOptions, + Annotated, }; use dynamo_parsers::tool_calling::try_tool_call_parse_aggregate; @@ -99,6 +101,7 @@ impl DeltaAggregator { /// * `Err(String)` if an error occurs during processing. pub async fn apply( stream: impl Stream>, + parsing_options: ParsingOptions, ) -> Result { let aggregator = stream .fold(DeltaAggregator::new(), |mut aggregator, delta| async move { @@ -175,7 +178,10 @@ impl DeltaAggregator { // After aggregation, inspect each choice's text for tool call syntax for choice in aggregator.choices.values_mut() { if choice.tool_calls.is_none() { - if let Ok(tool_calls) = try_tool_call_parse_aggregate(&choice.text, None) { + if let Ok(tool_calls) = try_tool_call_parse_aggregate( + &choice.text, + parsing_options.tool_call_parser.as_deref(), + ) { if tool_calls.is_empty() { continue; } @@ -262,6 +268,7 @@ pub trait ChatCompletionAggregator { /// * `Err(String)` if an error occurs. async fn from_annotated_stream( stream: impl Stream>, + parsing_options: ParsingOptions, ) -> Result; /// Converts an SSE stream into a [`NvCreateChatCompletionResponse`]. @@ -274,21 +281,24 @@ pub trait ChatCompletionAggregator { /// * `Err(String)` if an error occurs. async fn from_sse_stream( stream: DataStream>, + parsing_options: ParsingOptions, ) -> Result; } impl ChatCompletionAggregator for dynamo_async_openai::types::CreateChatCompletionResponse { async fn from_annotated_stream( stream: impl Stream>, + parsing_options: ParsingOptions, ) -> Result { - DeltaAggregator::apply(stream).await + DeltaAggregator::apply(stream, parsing_options).await } async fn from_sse_stream( stream: DataStream>, + parsing_options: ParsingOptions, ) -> Result { let stream = convert_sse_stream::(stream); - NvCreateChatCompletionResponse::from_annotated_stream(stream).await + NvCreateChatCompletionResponse::from_annotated_stream(stream, parsing_options).await } } @@ -347,7 +357,7 @@ mod tests { Box::pin(stream::empty()); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); @@ -377,7 +387,7 @@ mod tests { let stream = Box::pin(stream::iter(vec![annotated_delta])); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); @@ -421,7 +431,7 @@ mod tests { let stream = Box::pin(stream::iter(annotated_deltas)); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); @@ -492,7 +502,7 @@ mod tests { let stream = Box::pin(stream::iter(vec![annotated_delta])); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); @@ -550,7 +560,7 @@ mod tests { let stream = Box::pin(stream::iter(vec![annotated_delta])); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); diff --git a/lib/llm/src/protocols/openai/completions/aggregator.rs b/lib/llm/src/protocols/openai/completions/aggregator.rs index 7bb0c44aea..e72fc072c2 100644 --- a/lib/llm/src/protocols/openai/completions/aggregator.rs +++ b/lib/llm/src/protocols/openai/completions/aggregator.rs @@ -22,7 +22,9 @@ use super::NvCreateCompletionResponse; use crate::protocols::{ codec::{Message, SseCodecError}, common::FinishReason, - convert_sse_stream, Annotated, DataStream, + convert_sse_stream, + openai::ParsingOptions, + Annotated, DataStream, }; /// Aggregates a stream of [`CompletionResponse`]s into a single [`CompletionResponse`]. @@ -65,7 +67,9 @@ impl DeltaAggregator { /// Aggregates a stream of [`Annotated`]s into a single [`CompletionResponse`]. pub async fn apply( stream: impl Stream>, + parsing_options: ParsingOptions, ) -> Result { + tracing::debug!("Tool Call Parser: {:?}", parsing_options.tool_call_parser); // TODO: remove this once completion has tool call support let aggregator = stream .fold(DeltaAggregator::new(), |mut aggregator, delta| async move { let delta = match delta.ok() { @@ -177,15 +181,17 @@ impl From for dynamo_async_openai::types::Choice { impl NvCreateCompletionResponse { pub async fn from_sse_stream( stream: DataStream>, + parsing_options: ParsingOptions, ) -> Result { let stream = convert_sse_stream::(stream); - NvCreateCompletionResponse::from_annotated_stream(stream).await + NvCreateCompletionResponse::from_annotated_stream(stream, parsing_options).await } pub async fn from_annotated_stream( stream: impl Stream>, + parsing_options: ParsingOptions, ) -> Result { - DeltaAggregator::apply(stream).await + DeltaAggregator::apply(stream, parsing_options).await } } @@ -241,7 +247,7 @@ mod tests { let stream: DataStream> = Box::pin(stream::empty()); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); @@ -265,7 +271,7 @@ mod tests { let stream = Box::pin(stream::iter(vec![annotated_delta])); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); @@ -305,7 +311,7 @@ mod tests { let stream = Box::pin(stream::iter(annotated_deltas)); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); @@ -365,7 +371,7 @@ mod tests { let stream = Box::pin(stream::iter(vec![annotated_delta])); // Call DeltaAggregator::apply - let result = DeltaAggregator::apply(stream).await; + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; // Check the result assert!(result.is_ok()); diff --git a/lib/llm/tests/aggregators.rs b/lib/llm/tests/aggregators.rs index 5f16715e43..c6ad39dfa9 100644 --- a/lib/llm/tests/aggregators.rs +++ b/lib/llm/tests/aggregators.rs @@ -18,6 +18,7 @@ use dynamo_llm::protocols::{ openai::{ chat_completions::{aggregator::ChatCompletionAggregator, NvCreateChatCompletionResponse}, completions::NvCreateCompletionResponse, + ParsingOptions, }, ContentProvider, DataStream, }; @@ -37,9 +38,12 @@ async fn test_openai_chat_stream() { // note: we are only taking the first 16 messages to keep the size of the response small let stream = create_message_stream(&data).take(16); - let result = NvCreateChatCompletionResponse::from_sse_stream(Box::pin(stream)) - .await - .unwrap(); + let result = NvCreateChatCompletionResponse::from_sse_stream( + Box::pin(stream), + ParsingOptions::default(), + ) + .await + .unwrap(); // todo: provide a cleaner way to extract the content from choices assert_eq!( @@ -59,9 +63,12 @@ async fn test_openai_chat_stream() { #[tokio::test] async fn test_openai_chat_edge_case_multi_line_data() { let stream = create_stream(CHAT_ROOT_PATH, "edge_cases/valid-multi-line-data"); - let result = NvCreateChatCompletionResponse::from_sse_stream(Box::pin(stream)) - .await - .unwrap(); + let result = NvCreateChatCompletionResponse::from_sse_stream( + Box::pin(stream), + ParsingOptions::default(), + ) + .await + .unwrap(); assert_eq!( result @@ -79,9 +86,12 @@ async fn test_openai_chat_edge_case_multi_line_data() { #[tokio::test] async fn test_openai_chat_edge_case_comments_per_response() { let stream = create_stream(CHAT_ROOT_PATH, "edge_cases/valid-comments_per_response"); - let result = NvCreateChatCompletionResponse::from_sse_stream(Box::pin(stream)) - .await - .unwrap(); + let result = NvCreateChatCompletionResponse::from_sse_stream( + Box::pin(stream), + ParsingOptions::default(), + ) + .await + .unwrap(); assert_eq!( result @@ -99,7 +109,11 @@ async fn test_openai_chat_edge_case_comments_per_response() { #[tokio::test] async fn test_openai_chat_edge_case_invalid_deserialize_error() { let stream = create_stream(CHAT_ROOT_PATH, "edge_cases/invalid-deserialize_error"); - let result = NvCreateChatCompletionResponse::from_sse_stream(Box::pin(stream)).await; + let result = NvCreateChatCompletionResponse::from_sse_stream( + Box::pin(stream), + ParsingOptions::default(), + ) + .await; assert!(result.is_err()); // insta::assert_debug_snapshot!(result); @@ -112,9 +126,10 @@ async fn test_openai_chat_edge_case_invalid_deserialize_error() { #[tokio::test] async fn test_openai_cmpl_stream() { let stream = create_stream(CMPL_ROOT_PATH, "completion.streaming.1").take(16); - let result = NvCreateCompletionResponse::from_sse_stream(Box::pin(stream)) - .await - .unwrap(); + let result = + NvCreateCompletionResponse::from_sse_stream(Box::pin(stream), ParsingOptions::default()) + .await + .unwrap(); // todo: provide a cleaner way to extract the content from choices assert_eq!( diff --git a/lib/parsers/src/tool_calling/tools.rs b/lib/parsers/src/tool_calling/tools.rs index 96b88a59b4..7f326b46ad 100644 --- a/lib/parsers/src/tool_calling/tools.rs +++ b/lib/parsers/src/tool_calling/tools.rs @@ -14,6 +14,11 @@ pub fn try_tool_call_parse_aggregate( message: &str, parser_str: Option<&str>, ) -> anyhow::Result> { + if parser_str.is_none() { + tracing::info!("No tool parser provided. Trying parsing with default parser."); + } else { + tracing::info!("Using tool parser: {:?}", parser_str); + } let parsed = detect_and_parse_tool_call(message, parser_str)?; if parsed.is_empty() { return Ok(vec![]); From b25fbad06db57b7feb2d0c7939a81dfc5be3ad71 Mon Sep 17 00:00:00 2001 From: Richard Huo Date: Fri, 22 Aug 2025 14:41:41 -0700 Subject: [PATCH 08/82] =?UTF-8?q?fix:=20[TRTLLM+=20LLAMA4=20+=20Eagle=203]?= =?UTF-8?q?=20Remove=20the=20=E2=80=98two-models=20config=E2=80=99=20and?= =?UTF-8?q?=20set=20the=20=E2=80=98one-model=E2=80=99=20solution=20as=20th?= =?UTF-8?q?e=20default=20(#2661)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jason Zhou --- .../llama4/eagle/eagle_agg.yaml | 41 ------------------ .../eagle_agg.yml} | 24 ++++++----- .../llama4/eagle/eagle_decode.yaml | 8 ++-- .../llama4/eagle/eagle_prefill.yaml | 6 +-- .../llama4/eagle_one_model/eagle_agg.yml | 38 ---------------- .../llama4/eagle_one_model/eagle_decode.yaml | 43 ------------------- .../backends/trtllm/llama4_plus_eagle.md | 14 +----- 7 files changed, 19 insertions(+), 155 deletions(-) delete mode 100644 components/backends/trtllm/engine_configs/llama4/eagle/eagle_agg.yaml rename components/backends/trtllm/engine_configs/llama4/{eagle_one_model/eagle_prefill.yaml => eagle/eagle_agg.yml} (76%) delete mode 100644 components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_agg.yml delete mode 100644 components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_decode.yaml diff --git a/components/backends/trtllm/engine_configs/llama4/eagle/eagle_agg.yaml b/components/backends/trtllm/engine_configs/llama4/eagle/eagle_agg.yaml deleted file mode 100644 index 297a01595e..0000000000 --- a/components/backends/trtllm/engine_configs/llama4/eagle/eagle_agg.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -backend: pytorch -tensor_parallel_size: 4 -moe_expert_parallel_size: 4 -max_batch_size: 256 -# When max_num_tokens set to higher values, can cause OOM issues. -# Will be investigated in the future with TRTLLM team. -max_num_tokens: 1024 -max_seq_len: 8448 -enable_autotuner: false -disable_overlap_scheduler: true - -# Enable Speculative Decoding in the model engine -speculative_config: - decoding_type: Eagle - max_draft_len: 1 - speculative_model_dir: nvidia/Llama-4-Maverick-17B-128E-Eagle3 - eagle3_one_model: false - -kv_cache_config: - free_gpu_memory_fraction: 0.5 - enable_block_reuse: false - - -cuda_graph_config: - max_batch_size: 8 - diff --git a/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_prefill.yaml b/components/backends/trtllm/engine_configs/llama4/eagle/eagle_agg.yml similarity index 76% rename from components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_prefill.yaml rename to components/backends/trtllm/engine_configs/llama4/eagle/eagle_agg.yml index 1cfc62ab02..f4144e42ce 100644 --- a/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_prefill.yaml +++ b/components/backends/trtllm/engine_configs/llama4/eagle/eagle_agg.yml @@ -14,24 +14,26 @@ # limitations under the License. backend: pytorch -tensor_parallel_size: 8 -moe_expert_parallel_size: 8 -max_batch_size: 1 -max_num_tokens: 8192 -max_seq_len: 8192 -print_iter_log: true -disable_overlap_scheduler: true +tensor_parallel_size: 4 +moe_expert_parallel_size: 4 +max_batch_size: 192 +max_num_tokens: 3072 +disable_overlap_scheduler: false # Enable Speculative Decoding in the model engine speculative_config: decoding_type: Eagle max_draft_len: 3 speculative_model_dir: nvidia/Llama-4-Maverick-17B-128E-Eagle3 - eagle3_one_model: True + eagle3_one_model: true kv_cache_config: - free_gpu_memory_fraction: 0.5 + free_gpu_memory_fraction: 0.2 enable_block_reuse: false -cache_transceiver_config: - backend: default +cuda_graph_config: + enable_padding: true + batch_sizes: [1,2,3,4,5,6,7,8,16,32,48,64,128,190,191,192] + +print_iter_log: true + diff --git a/components/backends/trtllm/engine_configs/llama4/eagle/eagle_decode.yaml b/components/backends/trtllm/engine_configs/llama4/eagle/eagle_decode.yaml index 0b8d799bfb..171df484d8 100644 --- a/components/backends/trtllm/engine_configs/llama4/eagle/eagle_decode.yaml +++ b/components/backends/trtllm/engine_configs/llama4/eagle/eagle_decode.yaml @@ -17,23 +17,21 @@ backend: pytorch tensor_parallel_size: 4 moe_expert_parallel_size: 4 max_batch_size: 256 -max_num_tokens: 512 +max_num_tokens: 1024 # 8704 = 8192 ISL + 512 OSL max_seq_len: 8704 disable_overlap_scheduler: true -enable_autotuner: false # Enable Speculative Decoding in the model engine speculative_config: decoding_type: Eagle - max_draft_len: 1 + max_draft_len: 3 speculative_model_dir: nvidia/Llama-4-Maverick-17B-128E-Eagle3 - eagle3_one_model: false + eagle3_one_model: true kv_cache_config: free_gpu_memory_fraction: 0.5 enable_block_reuse: false - dtype: fp8 cuda_graph_config: enable_padding: true diff --git a/components/backends/trtllm/engine_configs/llama4/eagle/eagle_prefill.yaml b/components/backends/trtllm/engine_configs/llama4/eagle/eagle_prefill.yaml index b05181b226..ce3059f0b4 100644 --- a/components/backends/trtllm/engine_configs/llama4/eagle/eagle_prefill.yaml +++ b/components/backends/trtllm/engine_configs/llama4/eagle/eagle_prefill.yaml @@ -21,19 +21,17 @@ max_num_tokens: 8192 max_seq_len: 8192 print_iter_log: true disable_overlap_scheduler: true -enable_autotuner: false # Enable Speculative Decoding in the model engine speculative_config: decoding_type: Eagle - max_draft_len: 1 + max_draft_len: 3 speculative_model_dir: nvidia/Llama-4-Maverick-17B-128E-Eagle3 - eagle3_one_model: false + eagle3_one_model: true kv_cache_config: free_gpu_memory_fraction: 0.5 enable_block_reuse: false - dtype: fp8 cache_transceiver_config: backend: default diff --git a/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_agg.yml b/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_agg.yml deleted file mode 100644 index cada38087c..0000000000 --- a/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_agg.yml +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -backend: pytorch -tensor_parallel_size: 8 -moe_expert_parallel_size: 8 -max_batch_size: 8 -max_num_tokens: 4096 -disable_overlap_scheduler: true # disable_overlap_scheduler is having acc issue on both aggregated and disaggregated serving - -# Enable Speculative Decoding in the model engine -speculative_config: - decoding_type: Eagle - max_draft_len: 3 - speculative_model_dir: nvidia/Llama-4-Maverick-17B-128E-Eagle3 - eagle3_one_model: true - -kv_cache_config: - free_gpu_memory_fraction: 0.5 - enable_block_reuse: false # true when target and draft are same kv dtype - -cuda_graph_config: - padding_enabled: true - max_batch_size: 8 - -print_iter_log: true diff --git a/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_decode.yaml b/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_decode.yaml deleted file mode 100644 index 43f04e2715..0000000000 --- a/components/backends/trtllm/engine_configs/llama4/eagle_one_model/eagle_decode.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -backend: pytorch -tensor_parallel_size: 8 -moe_expert_parallel_size: 8 -max_batch_size: 256 -max_num_tokens: 1024 -# 8704 = 8192 ISL + 512 OSL -max_seq_len: 8704 -disable_overlap_scheduler: true - -# Enable Speculative Decoding in the model engine -speculative_config: - decoding_type: Eagle - max_draft_len: 3 - speculative_model_dir: nvidia/Llama-4-Maverick-17B-128E-Eagle3 - eagle3_one_model: True - -kv_cache_config: - free_gpu_memory_fraction: 0.5 - enable_block_reuse: false - -cuda_graph_config: - padding_enabled: true - max_batch_size: 256 - -print_iter_log: true - -cache_transceiver_config: - backend: default diff --git a/components/backends/trtllm/llama4_plus_eagle.md b/components/backends/trtllm/llama4_plus_eagle.md index 2d542f7a1a..201b185243 100644 --- a/components/backends/trtllm/llama4_plus_eagle.md +++ b/components/backends/trtllm/llama4_plus_eagle.md @@ -30,16 +30,7 @@ This guide demonstrates how to deploy Llama 4 Maverick Instruct with Eagle Specu For advanced control over how requests are routed between prefill and decode workers in disaggregated mode, refer to the [Disaggregation Strategy](./README.md#disaggregation-strategy) section. ## Notes -* To run Eagle Speculative Decoding with Llama 4, ensure the container meets the following criteria: - * Built with a version of TensorRT-LLM based on the 0.21 release [Link](https://github.com/NVIDIA/TensorRT-LLM/tree/release/0.21) -* If you need to download model weights off huggingface, make sure you run the command `huggingface-cli login` and have access to the necessary gated models. - -## Eagle3-one-model -* Eagle3-one-model (`eagle3_one_model=True`) config is added in `engine_configs/llama4/eagle_one_model`. Build dynamo with the latest commit `66f299a` in TRTLLM 1.0.0.rc2 [Link](https://github.com/NVIDIA/TensorRT-LLM/commits/v1.0.0rc2/). -* The configs in `engine_configs/llama4/eagle_one_model` are tested with 8xH100 cluster. Be sure to change the `NUM_GPUS_PER_NODE` accordingly or change TP/EP size in config. 1 8xH100 node for aggregated .yml file, 2 8xH100 for prefill/decode .yml file. -* The current `./multinode/start_frontend_services.sh` may got ran `NUM_GPUS_PER_NODE` times depending on how srun/mpi is launched, beware that the frontend service only needs to be ran once. -* Eagle3-one-model appends the eagle3 layer at the end of the TRTLLM engine, instead of sending base/draft requests between 2 engines. Visit TRTLLM for more information. - +* Make sure the (`eagle3_one_model: true`) is set in the LLM API config inside the `engine_configs/llama4/eagle` folder. ## Setup @@ -66,7 +57,6 @@ export NUM_NODES=1 export ENGINE_CONFIG="/mnt/engine_configs/llama4/eagle/eagle_agg.yaml" ./multinode/srun_aggregated.sh ``` -* Known Issue: In Aggregated Serving, setting `max_num_tokens` to higher values (e.g. `max_num_tokens: 8448`) can lead to Out of Memory (OOM) errors. This is being investigated by the TRTLLM team. ## Disaggregated Serving @@ -77,8 +67,6 @@ export NUM_DECODE_NODES=1 export DECODE_ENGINE_CONFIG="/mnt/engine_configs/llama4/eagle/eagle_decode.yaml" ./multinode/srun_disaggregated.sh ``` -* Known Issue: In Aggregated Serving, setting `max_num_tokens` to higher values (e.g. `max_num_tokens: 8448`) can lead to Out of Memory (OOM) errors. This is being investigated by the TRTLLM team. - ## Example Request From f668a0b6c6a70a83f3aea1d3625bf0a09dbc1181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wa=C3=ABl=20Boukhobza?= Date: Fri, 22 Aug 2025 15:26:32 -0700 Subject: [PATCH 09/82] fix: handle missing span_name in logging test assertions (#2665) Signed-off-by: Jason Zhou --- lib/runtime/src/logging.rs | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/runtime/src/logging.rs b/lib/runtime/src/logging.rs index 0caef04c62..3b08b40db5 100644 --- a/lib/runtime/src/logging.rs +++ b/lib/runtime/src/logging.rs @@ -1009,28 +1009,40 @@ pub mod tests { // Parent span has no parent_id for log_line in &lines { - if log_line.get("span_name").unwrap().as_str().unwrap() == "parent" { - assert!(log_line.get("parent_id").is_none()); + if let Some(span_name) = log_line.get("span_name") { + if let Some(span_name_str) = span_name.as_str() { + if span_name_str == "parent" { + assert!(log_line.get("parent_id").is_none()); + } + } } } // Child span's parent_id is parent_span_id for log_line in &lines { - if log_line.get("span_name").unwrap().as_str().unwrap() == "child" { - assert_eq!( - log_line.get("parent_id").unwrap().as_str().unwrap(), - &parent_span_id - ); + if let Some(span_name) = log_line.get("span_name") { + if let Some(span_name_str) = span_name.as_str() { + if span_name_str == "child" { + assert_eq!( + log_line.get("parent_id").unwrap().as_str().unwrap(), + &parent_span_id + ); + } + } } } // Grandchild span's parent_id is child_span_id for log_line in &lines { - if log_line.get("span_name").unwrap().as_str().unwrap() == "grandchild" { - assert_eq!( - log_line.get("parent_id").unwrap().as_str().unwrap(), - &child_span_id - ); + if let Some(span_name) = log_line.get("span_name") { + if let Some(span_name_str) = span_name.as_str() { + if span_name_str == "grandchild" { + assert_eq!( + log_line.get("parent_id").unwrap().as_str().unwrap(), + &child_span_id + ); + } + } } } From 5f72551081966dad48033c13050df73149336240 Mon Sep 17 00:00:00 2001 From: Hongkuan Zhou Date: Fri, 22 Aug 2025 15:51:58 -0700 Subject: [PATCH 10/82] fix: missing tokenizer args in sla_planner.py (#2667) Signed-off-by: Jason Zhou --- benchmarks/profiler/profile_sla.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/benchmarks/profiler/profile_sla.py b/benchmarks/profiler/profile_sla.py index fd5435b88e..e9173f48d6 100644 --- a/benchmarks/profiler/profile_sla.py +++ b/benchmarks/profiler/profile_sla.py @@ -158,7 +158,11 @@ async def run_profile(args): base_url = client.get_service_url() genai_perf_artifact_dir = f"{work_dir}/gap_isl{args.isl}" gap_result = benchmark_prefill( - args.isl, genai_perf_artifact_dir, model_name, base_url=base_url + args.isl, + genai_perf_artifact_dir, + model_name, + model_name, + base_url=base_url, ) if gap_result is not None: ttft = gap_result["time_to_first_token"]["avg"] @@ -283,6 +287,7 @@ async def run_profile(args): num_request, genai_perf_artifact_dir, model_name, + model_name, base_url=base_url, ) if gap_result is not None: From c3f6ec046f76cc1fc05740f9dfc74fd7c84ccece Mon Sep 17 00:00:00 2001 From: Graham King Date: Fri, 22 Aug 2025 19:17:15 -0400 Subject: [PATCH 11/82] chore: Rust to 1.89 and edition 2024 (#2659) Signed-off-by: Jason Zhou --- Cargo.toml | 2 +- Earthfile | 4 +- components/metrics/src/bin/mock_worker.rs | 12 +- components/metrics/src/lib.rs | 6 +- components/metrics/src/main.rs | 11 +- components/router/src/main.rs | 4 +- container/Dockerfile | 2 +- container/Dockerfile.kvbm | 2 +- container/Dockerfile.sglang | 2 +- container/Dockerfile.sglang-wideep | 14 +- container/Dockerfile.trtllm | 2 +- container/Dockerfile.trtllm_prebuilt | 2 +- container/Dockerfile.vllm | 2 +- launch/dynamo-run/src/flags.rs | 18 +- launch/dynamo-run/src/lib.rs | 4 +- launch/dynamo-run/src/main.rs | 8 +- lib/async-openai/src/assistants.rs | 2 +- lib/async-openai/src/audio.rs | 2 +- lib/async-openai/src/audit_logs.rs | 2 +- lib/async-openai/src/batches.rs | 2 +- lib/async-openai/src/chat.rs | 2 +- lib/async-openai/src/client.rs | 10 +- lib/async-openai/src/config.rs | 4 +- lib/async-openai/src/download.rs | 4 +- lib/async-openai/src/embedding.rs | 2 +- lib/async-openai/src/file.rs | 2 +- lib/async-openai/src/fine_tuning.rs | 2 +- lib/async-openai/src/image.rs | 2 +- lib/async-openai/src/invites.rs | 2 +- lib/async-openai/src/messages.rs | 2 +- lib/async-openai/src/model.rs | 2 +- lib/async-openai/src/moderation.rs | 2 +- lib/async-openai/src/project_api_keys.rs | 2 +- .../src/project_service_accounts.rs | 2 +- lib/async-openai/src/project_users.rs | 2 +- lib/async-openai/src/projects.rs | 2 +- lib/async-openai/src/responses.rs | 2 +- lib/async-openai/src/runs.rs | 2 +- lib/async-openai/src/steps.rs | 2 +- lib/async-openai/src/threads.rs | 2 +- .../src/types/assistant_stream.rs | 2 +- lib/async-openai/src/types/embedding.rs | 2 +- lib/async-openai/src/types/impls.rs | 2 +- lib/async-openai/src/uploads.rs | 2 +- lib/async-openai/src/users.rs | 2 +- lib/async-openai/src/util.rs | 2 +- .../src/vector_store_file_batches.rs | 2 +- lib/async-openai/src/vector_store_files.rs | 2 +- lib/async-openai/src/vector_stores.rs | 2 +- lib/async-openai/tests/bring-your-own-type.rs | 4 +- lib/async-openai/tests/whisper.rs | 2 +- lib/bindings/c/src/lib.rs | 16 +- lib/engines/llamacpp/src/lib.rs | 8 +- lib/engines/mistralrs/src/lib.rs | 23 ++- lib/llm/benches/tokenizer.rs | 4 +- lib/llm/src/backend.rs | 98 +++++----- lib/llm/src/block_manager.rs | 8 +- lib/llm/src/block_manager/block.rs | 22 +-- lib/llm/src/block_manager/block/data.rs | 20 +- lib/llm/src/block_manager/block/data/local.rs | 8 +- .../src/block_manager/block/data/logical.rs | 2 +- lib/llm/src/block_manager/block/locality.rs | 2 +- lib/llm/src/block_manager/block/registry.rs | 44 ++--- lib/llm/src/block_manager/block/state.rs | 2 +- lib/llm/src/block_manager/block/transfer.rs | 2 +- .../block_manager/block/transfer/context.rs | 10 +- .../src/block_manager/block/transfer/cuda.rs | 22 ++- .../block_manager/block/transfer/memcpy.rs | 2 +- .../src/block_manager/connector/protocol.rs | 23 ++- lib/llm/src/block_manager/controller.rs | 4 +- lib/llm/src/block_manager/distributed.rs | 8 +- .../src/block_manager/distributed/transfer.rs | 10 +- .../src/block_manager/distributed/worker.rs | 8 +- lib/llm/src/block_manager/distributed/zmq.rs | 10 +- lib/llm/src/block_manager/layout/nixl.rs | 2 +- lib/llm/src/block_manager/metrics.rs | 4 +- lib/llm/src/block_manager/offload.rs | 46 ++--- lib/llm/src/block_manager/offload/pending.rs | 6 +- lib/llm/src/block_manager/offload/request.rs | 2 +- lib/llm/src/block_manager/pool.rs | 6 +- lib/llm/src/block_manager/pool/managed.rs | 2 +- .../src/block_manager/pool/managed/active.rs | 22 +-- .../block_manager/pool/managed/inactive.rs | 10 +- .../src/block_manager/pool/managed/state.rs | 16 +- lib/llm/src/block_manager/state.rs | 6 +- lib/llm/src/block_manager/storage.rs | 7 +- lib/llm/src/block_manager/storage/arena.rs | 2 +- lib/llm/src/block_manager/storage/cuda.rs | 2 +- lib/llm/src/block_manager/storage/disk.rs | 2 +- lib/llm/src/block_manager/storage/nixl.rs | 6 +- lib/llm/src/cuda.rs | 2 +- lib/llm/src/disagg_router.rs | 36 ++-- lib/llm/src/discovery/model_manager.rs | 2 +- lib/llm/src/discovery/watcher.rs | 25 ++- lib/llm/src/engines.rs | 2 +- lib/llm/src/entrypoint/input/batch.rs | 6 +- lib/llm/src/entrypoint/input/common.rs | 26 +-- lib/llm/src/entrypoint/input/endpoint.rs | 15 +- lib/llm/src/entrypoint/input/http.rs | 6 +- lib/llm/src/entrypoint/input/text.rs | 4 +- lib/llm/src/gguf/content.rs | 6 +- lib/llm/src/gguf/gguf_metadata.rs | 2 +- lib/llm/src/gguf/gguf_tokenizer.rs | 4 +- lib/llm/src/http/client.rs | 6 +- lib/llm/src/http/service/health.rs | 4 +- lib/llm/src/http/service/metrics.rs | 4 +- lib/llm/src/http/service/openai.rs | 52 +++--- lib/llm/src/http/service/service_v2.rs | 4 +- lib/llm/src/kv_router.rs | 10 +- lib/llm/src/kv_router/approx.rs | 6 +- lib/llm/src/kv_router/indexer.rs | 18 +- lib/llm/src/kv_router/metrics_aggregator.rs | 6 +- lib/llm/src/kv_router/publisher.rs | 12 +- lib/llm/src/kv_router/scheduler.rs | 8 +- lib/llm/src/kv_router/sequence.rs | 26 +-- lib/llm/src/lib.rs | 14 +- lib/llm/src/local_model.rs | 20 +- lib/llm/src/local_model/runtime_config.rs | 2 +- lib/llm/src/migration.rs | 35 ++-- lib/llm/src/mocker/engine.rs | 16 +- lib/llm/src/mocker/kv_manager.rs | 38 ++-- lib/llm/src/mocker/protocols.rs | 72 ++++---- lib/llm/src/mocker/scheduler.rs | 20 +- lib/llm/src/perf/logprobs.rs | 2 +- lib/llm/src/preprocessor.rs | 26 +-- .../prompt/template/formatters.rs | 18 +- .../preprocessor/prompt/template/tokcfg.rs | 2 +- lib/llm/src/protocols/codec.rs | 16 +- lib/llm/src/protocols/common/llm_backend.rs | 2 +- lib/llm/src/protocols/openai.rs | 12 +- .../src/protocols/openai/chat_completions.rs | 4 +- .../openai/chat_completions/aggregator.rs | 49 ++--- lib/llm/src/protocols/openai/completions.rs | 16 +- .../openai/completions/aggregator.rs | 14 +- .../protocols/openai/embeddings/aggregator.rs | 38 ++-- lib/llm/src/protocols/openai/validate.rs | 174 +++++++++--------- lib/llm/src/recorder.rs | 101 +++++----- lib/llm/src/tokenizers/hf.rs | 2 +- lib/llm/src/tokens.rs | 8 +- lib/llm/tests/aggregators.rs | 8 +- lib/llm/tests/block_manager.rs | 12 +- lib/llm/tests/http-service.rs | 42 ++--- lib/llm/tests/http_metrics.rs | 8 +- lib/llm/tests/logprob_analysis_integration.rs | 4 +- lib/llm/tests/preprocessor.rs | 2 +- lib/llm/tests/test_common_ext.rs | 2 +- lib/parsers/src/tool_calling/mod.rs | 4 +- lib/parsers/src/tool_calling/parsers.rs | 4 +- lib/parsers/src/tool_calling/tools.rs | 2 +- lib/runtime/examples/Cargo.lock | 10 + lib/runtime/examples/rust-toolchain.toml | 2 +- lib/runtime/src/component.rs | 19 +- lib/runtime/src/component/endpoint.rs | 13 +- lib/runtime/src/config.rs | 16 +- lib/runtime/src/discovery.rs | 2 +- lib/runtime/src/distributed.rs | 8 +- lib/runtime/src/instances.rs | 2 +- lib/runtime/src/lib.rs | 2 +- lib/runtime/src/logging.rs | 84 ++++----- lib/runtime/src/metrics.rs | 36 ++-- lib/runtime/src/pipeline.rs | 4 +- lib/runtime/src/pipeline/context.rs | 2 +- lib/runtime/src/pipeline/error.rs | 2 +- lib/runtime/src/pipeline/network.rs | 4 +- .../src/pipeline/network/codec/two_part.rs | 16 +- .../network/egress/addressed_router.rs | 6 +- .../pipeline/network/egress/push_router.rs | 12 +- .../pipeline/network/ingress/push_endpoint.rs | 7 +- .../pipeline/network/ingress/push_handler.rs | 12 +- lib/runtime/src/pipeline/network/tcp.rs | 4 +- .../src/pipeline/network/tcp/client.rs | 4 +- .../src/pipeline/network/tcp/server.rs | 17 +- lib/runtime/src/pipeline/nodes/sinks.rs | 2 +- .../src/pipeline/nodes/sources/base.rs | 2 +- lib/runtime/src/protocols/annotated.rs | 33 ++-- lib/runtime/src/runtime.rs | 2 +- lib/runtime/src/service.rs | 6 +- lib/runtime/src/slug.rs | 12 +- lib/runtime/src/storage/key_value_store.rs | 4 +- .../src/storage/key_value_store/etcd.rs | 9 +- .../src/storage/key_value_store/mem.rs | 2 +- lib/runtime/src/system_status_server.rs | 2 +- lib/runtime/src/transports/etcd.rs | 8 +- lib/runtime/src/transports/nats.rs | 4 +- lib/runtime/src/transports/zmq.rs | 4 +- .../src/utils/leader_worker_barrier.rs | 4 +- lib/runtime/src/utils/pool.rs | 2 +- lib/runtime/src/utils/stream.rs | 2 +- lib/runtime/src/utils/tasks/critical.rs | 8 +- lib/runtime/src/utils/tasks/tracker.rs | 44 +++-- lib/runtime/src/utils/typed_prefix_watcher.rs | 2 +- lib/runtime/src/utils/worker_monitor.rs | 2 +- lib/runtime/src/worker.rs | 7 +- lib/runtime/tests/common/engines.rs | 2 +- lib/runtime/tests/common/mock.rs | 2 +- lib/runtime/tests/lifecycle.rs | 2 +- lib/runtime/tests/pipeline.rs | 20 +- lib/runtime/tests/soak.rs | 10 +- rust-toolchain.toml | 2 +- 199 files changed, 1120 insertions(+), 1120 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 02906528fd..bd6620cf20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ resolver = "3" [workspace.package] version = "0.4.1" -edition = "2021" +edition = "2024" description = "Dynamo Inference Framework" authors = ["NVIDIA Inc. "] license = "Apache-2.0" diff --git a/Earthfile b/Earthfile index 49844fc302..590dc8eb86 100644 --- a/Earthfile +++ b/Earthfile @@ -91,13 +91,13 @@ rust-base: ENV RUSTUP_HOME=/usr/local/rustup ENV CARGO_HOME=/usr/local/cargo ENV PATH=/usr/local/cargo/bin:$PATH - ENV RUST_VERSION=1.87.0 + ENV RUST_VERSION=1.89.0 ENV RUSTARCH=x86_64-unknown-linux-gnu RUN wget --tries=3 --waitretry=5 "https://static.rust-lang.org/rustup/archive/1.28.1/x86_64-unknown-linux-gnu/rustup-init" && \ echo "a3339fb004c3d0bb9862ba0bce001861fe5cbde9c10d16591eb3f39ee6cd3e7f *rustup-init" | sha256sum -c - && \ chmod +x rustup-init && \ - ./rustup-init -y --no-modify-path --profile minimal --default-toolchain 1.87.0 --default-host x86_64-unknown-linux-gnu && \ + ./rustup-init -y --no-modify-path --profile minimal --default-toolchain 1.89.0 --default-host x86_64-unknown-linux-gnu && \ rm rustup-init && \ chmod -R a+w $RUSTUP_HOME $CARGO_HOME diff --git a/components/metrics/src/bin/mock_worker.rs b/components/metrics/src/bin/mock_worker.rs index 07cd29c751..4b5c696ad2 100644 --- a/components/metrics/src/bin/mock_worker.rs +++ b/components/metrics/src/bin/mock_worker.rs @@ -14,25 +14,25 @@ // limitations under the License. use dynamo_llm::kv_router::{ + KV_HIT_RATE_SUBJECT, protocols::{ForwardPassMetrics, KvStats, WorkerStats}, scheduler::KVHitRateEvent, - KV_HIT_RATE_SUBJECT, }; use dynamo_runtime::{ - component::{service::EndpointStats, Namespace}, + DistributedRuntime, Result, Runtime, Worker, + component::{Namespace, service::EndpointStats}, logging, pipeline::{ - async_trait, network::Ingress, AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, - ResponseStream, SingleIn, + AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, ResponseStream, SingleIn, + async_trait, network::Ingress, }, protocols::annotated::Annotated, stream, traits::events::EventPublisher, - DistributedRuntime, Result, Runtime, Worker, }; use rand::Rng; use std::sync::Arc; -use tokio::time::{interval, Duration}; +use tokio::time::{Duration, interval}; fn main() -> Result<()> { logging::init(); diff --git a/components/metrics/src/lib.rs b/components/metrics/src/lib.rs index 5f9ddec6c4..df675bf647 100644 --- a/components/metrics/src/lib.rs +++ b/components/metrics/src/lib.rs @@ -76,8 +76,8 @@ //! Ok(()) //! } -use axum::{routing::get, Router}; -use prometheus::{register_counter_vec, register_gauge_vec, Encoder, TextEncoder}; +use axum::{Router, routing::get}; +use prometheus::{Encoder, TextEncoder, register_counter_vec, register_gauge_vec}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; @@ -88,7 +88,7 @@ use dynamo_llm::kv_router::scoring::Endpoint; use dynamo_llm::kv_router::scoring::ProcessedEndpoints; use dynamo_runtime::{ - distributed::Component, error, service::EndpointInfo, utils::Duration, Result, + Result, distributed::Component, error, service::EndpointInfo, utils::Duration, }; /// Configuration for metrics collection mode diff --git a/components/metrics/src/main.rs b/components/metrics/src/main.rs index 9a451b1a8d..53f9e49e78 100644 --- a/components/metrics/src/main.rs +++ b/components/metrics/src/main.rs @@ -27,21 +27,20 @@ //! - ISL Blocks: Cumulative count of total blocks in all KV hit rate events //! - Overlap Blocks: Cumulative count of blocks that were already in the KV cache use clap::Parser; -use dynamo_llm::kv_router::scheduler::KVHitRateEvent; use dynamo_llm::kv_router::KV_HIT_RATE_SUBJECT; +use dynamo_llm::kv_router::scheduler::KVHitRateEvent; use dynamo_runtime::{ - error, logging, + DistributedRuntime, ErrorContext, Result, Runtime, Worker, error, logging, traits::events::{EventPublisher, EventSubscriber}, utils::{Duration, Instant}, - DistributedRuntime, ErrorContext, Result, Runtime, Worker, }; use futures::stream::StreamExt; use std::sync::Arc; // Import from our library use metrics::{ - collect_endpoints, extract_metrics, postprocess_metrics, LLMWorkerLoadCapacityConfig, - MetricsMode, PrometheusMetricsCollector, + LLMWorkerLoadCapacityConfig, MetricsMode, PrometheusMetricsCollector, collect_endpoints, + extract_metrics, postprocess_metrics, }; /// CLI arguments for the metrics application @@ -274,7 +273,7 @@ mod tests { #[test] fn test_namespace_from_env() { - env::set_var("DYN_NAMESPACE", "test-namespace"); + unsafe { env::set_var("DYN_NAMESPACE", "test-namespace") }; let args = Args::parse_from(["count", "--component", "comp", "--endpoint", "end"]); assert_eq!(args.namespace, "test-namespace"); } diff --git a/components/router/src/main.rs b/components/router/src/main.rs index 8961a5fac9..b5b2f8f7a8 100644 --- a/components/router/src/main.rs +++ b/components/router/src/main.rs @@ -26,13 +26,13 @@ use std::sync::Arc; use clap::Parser; use dynamo_llm::kv_router::{ + KvRouter, WorkerSelector, protocols::WorkerSelectionResult, scheduler::{DefaultWorkerSelector, KvSchedulerError, SchedulingRequest}, - KvRouter, WorkerSelector, }; use dynamo_llm::local_model::runtime_config::ModelRuntimeConfig; use dynamo_runtime::{ - logging, pipeline::network::Ingress, DistributedRuntime, Result, Runtime, Worker, + DistributedRuntime, Result, Runtime, Worker, logging, pipeline::network::Ingress, }; #[derive(Parser)] diff --git a/container/Dockerfile b/container/Dockerfile index c096e1029d..cc30a18ba4 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -53,7 +53,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.87.0 + RUST_VERSION=1.89.0 WORKDIR /opt/dynamo diff --git a/container/Dockerfile.kvbm b/container/Dockerfile.kvbm index 4bbd6833de..103b0dd5b4 100644 --- a/container/Dockerfile.kvbm +++ b/container/Dockerfile.kvbm @@ -220,7 +220,7 @@ RUN apt update -y && \ ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.87.0 + RUST_VERSION=1.89.0 # Define Rust target based on ARCH_ALT ARG ARG RUSTARCH=${ARCH_ALT}-unknown-linux-gnu diff --git a/container/Dockerfile.sglang b/container/Dockerfile.sglang index 5df72344a7..4c1bc8fcaa 100644 --- a/container/Dockerfile.sglang +++ b/container/Dockerfile.sglang @@ -212,7 +212,7 @@ RUN apt update -y && \ ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.87.0 + RUST_VERSION=1.89.0 # Define Rust target based on ARCH_ALT ARG ARG RUSTARCH=${ARCH_ALT}-unknown-linux-gnu diff --git a/container/Dockerfile.sglang-wideep b/container/Dockerfile.sglang-wideep index 00fe9b9b04..723eb9a82b 100644 --- a/container/Dockerfile.sglang-wideep +++ b/container/Dockerfile.sglang-wideep @@ -1,17 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. ARG SGLANG_IMAGE_TAG="v0.5.0rc2-cu126" @@ -23,7 +11,7 @@ ARG ARCH_ALT="x86_64" ARG NIXL_UCX_REF="v1.19.0" ARG NIXL_TAG="0.5.0" ARG CMAKE_VERSION="3.31.8" -ARG RUST_VERSION="1.87.0" +ARG RUST_VERSION="1.89.0" ARG CARGO_BUILD_JOBS="16" RUN apt-get update -y && \ diff --git a/container/Dockerfile.trtllm b/container/Dockerfile.trtllm index 4a73546a78..91ce53cc80 100644 --- a/container/Dockerfile.trtllm +++ b/container/Dockerfile.trtllm @@ -188,7 +188,7 @@ RUN apt-get update && \ ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.87.0 + RUST_VERSION=1.89.0 # Define Rust target based on ARCH_ALT ARG ARG RUSTARCH=${ARCH_ALT}-unknown-linux-gnu diff --git a/container/Dockerfile.trtllm_prebuilt b/container/Dockerfile.trtllm_prebuilt index ba53e2ab2b..28558610e1 100644 --- a/container/Dockerfile.trtllm_prebuilt +++ b/container/Dockerfile.trtllm_prebuilt @@ -41,7 +41,7 @@ ARG RUSTARCH=${ARCH_ALT}-unknown-linux-gnu ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.87.0 + RUST_VERSION=1.89.0 # Install Rust using RUSTARCH derived from ARCH_ALT RUN wget --tries=3 --waitretry=5 "https://static.rust-lang.org/rustup/archive/1.28.1/${RUSTARCH}/rustup-init" && \ diff --git a/container/Dockerfile.vllm b/container/Dockerfile.vllm index d55fe262d7..12786af768 100644 --- a/container/Dockerfile.vllm +++ b/container/Dockerfile.vllm @@ -238,7 +238,7 @@ RUN ARCH=$(dpkg --print-architecture) && \ ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.87.0 + RUST_VERSION=1.89.0 # Define Rust target based on ARCH_ALT ARG ARG RUSTARCH=${ARCH_ALT}-unknown-linux-gnu diff --git a/launch/dynamo-run/src/flags.rs b/launch/dynamo-run/src/flags.rs index 54d3fc4890..d02458eda4 100644 --- a/launch/dynamo-run/src/flags.rs +++ b/launch/dynamo-run/src/flags.rs @@ -17,8 +17,8 @@ use std::collections::HashMap; use std::path::PathBuf; use clap::ValueEnum; -use dynamo_llm::entrypoint::input::Input; use dynamo_llm::entrypoint::RouterConfig; +use dynamo_llm::entrypoint::input::Input; use dynamo_llm::kv_router::KvRouterConfig; use dynamo_llm::local_model::LocalModel; use dynamo_llm::mocker::protocols::MockEngineArgs; @@ -176,13 +176,19 @@ impl Flags { match out_opt { Output::Auto => { if self.context_length.is_some() { - anyhow::bail!("'--context-length' flag should only be used on the worker node, not on the ingress"); + anyhow::bail!( + "'--context-length' flag should only be used on the worker node, not on the ingress" + ); } if self.kv_cache_block_size.is_some() { - anyhow::bail!("'--kv-cache-block-size' flag should only be used on the worker node, not on the ingress"); + anyhow::bail!( + "'--kv-cache-block-size' flag should only be used on the worker node, not on the ingress" + ); } if self.migration_limit.is_some() { - anyhow::bail!("'--migration-limit' flag should only be used on the worker node, not on the ingress"); + anyhow::bail!( + "'--migration-limit' flag should only be used on the worker node, not on the ingress" + ); } } Output::Static(_) => { @@ -211,7 +217,9 @@ impl Flags { #[cfg(feature = "llamacpp")] Output::LlamaCpp => { if !local_model.path().is_file() { - anyhow::bail!("--model-path should refer to a GGUF file. llama_cpp does not support safetensors."); + anyhow::bail!( + "--model-path should refer to a GGUF file. llama_cpp does not support safetensors." + ); } } Output::Mocker => { diff --git a/launch/dynamo-run/src/lib.rs b/launch/dynamo-run/src/lib.rs index 0d1c9bdf9a..e078bfdf1a 100644 --- a/launch/dynamo-run/src/lib.rs +++ b/launch/dynamo-run/src/lib.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::Context as _; -use dynamo_llm::entrypoint::input::Input; use dynamo_llm::entrypoint::EngineConfig; +use dynamo_llm::entrypoint::input::Input; use dynamo_llm::local_model::{LocalModel, LocalModelBuilder}; -use dynamo_runtime::distributed::DistributedConfig; use dynamo_runtime::CancellationToken; +use dynamo_runtime::distributed::DistributedConfig; use dynamo_runtime::{DistributedRuntime, Runtime}; mod flags; diff --git a/launch/dynamo-run/src/main.rs b/launch/dynamo-run/src/main.rs index 348287e770..5a2e39f34d 100644 --- a/launch/dynamo-run/src/main.rs +++ b/launch/dynamo-run/src/main.rs @@ -38,14 +38,14 @@ fn main() -> anyhow::Result<()> { _ => { return Err(anyhow::anyhow!( "Invalid verbosity level. Valid values are v (debug) or vv (trace)" - )) + )); } }, Err(_) => "info", }; if log_level != "info" { - std::env::set_var("DYN_LOG", log_level); + unsafe { std::env::set_var("DYN_LOG", log_level) }; } logging::init(); @@ -94,7 +94,9 @@ async fn wrapper(runtime: dynamo_runtime::Runtime) -> anyhow::Result<()> { } "out" => { if val == "sglang" || val == "trtllm" || val == "vllm" { - tracing::error!("To run the {val} engine please use the Python interface, see root README or look in directory `components/backends/`."); + tracing::error!( + "To run the {val} engine please use the Python interface, see root README or look in directory `components/backends/`." + ); std::process::exit(1); } diff --git a/lib/async-openai/src/assistants.rs b/lib/async-openai/src/assistants.rs index ab77420427..27363a3c01 100644 --- a/lib/async-openai/src/assistants.rs +++ b/lib/async-openai/src/assistants.rs @@ -11,13 +11,13 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ AssistantObject, CreateAssistantRequest, DeleteAssistantResponse, ListAssistantsResponse, ModifyAssistantRequest, }, - Client, }; /// Build assistants that can call models and use tools to perform tasks. diff --git a/lib/async-openai/src/audio.rs b/lib/async-openai/src/audio.rs index 9d33a13d52..e029b6cdb0 100644 --- a/lib/async-openai/src/audio.rs +++ b/lib/async-openai/src/audio.rs @@ -11,6 +11,7 @@ use bytes::Bytes; use crate::{ + Client, config::Config, error::OpenAIError, types::{ @@ -19,7 +20,6 @@ use crate::{ CreateTranslationRequest, CreateTranslationResponseJson, CreateTranslationResponseVerboseJson, }, - Client, }; /// Turn audio into text or text into audio. diff --git a/lib/async-openai/src/audit_logs.rs b/lib/async-openai/src/audit_logs.rs index 07aed9d628..0fcf7a5e07 100644 --- a/lib/async-openai/src/audit_logs.rs +++ b/lib/async-openai/src/audit_logs.rs @@ -10,7 +10,7 @@ use serde::Serialize; -use crate::{config::Config, error::OpenAIError, types::ListAuditLogsResponse, Client}; +use crate::{Client, config::Config, error::OpenAIError, types::ListAuditLogsResponse}; /// Logs of user actions and configuration changes within this organization. /// To log events, you must activate logging in the [Organization Settings](https://platform.openai.com/settings/organization/general). diff --git a/lib/async-openai/src/batches.rs b/lib/async-openai/src/batches.rs index 37b7014647..41e3d6779f 100644 --- a/lib/async-openai/src/batches.rs +++ b/lib/async-openai/src/batches.rs @@ -11,10 +11,10 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{Batch, BatchRequest, ListBatchesResponse}, - Client, }; /// Create large batches of API requests for asynchronous processing. The Batch API returns completions within 24 hours for a 50% discount. diff --git a/lib/async-openai/src/chat.rs b/lib/async-openai/src/chat.rs index 69852cc8f3..0bca5648b3 100644 --- a/lib/async-openai/src/chat.rs +++ b/lib/async-openai/src/chat.rs @@ -9,12 +9,12 @@ // Licensed under Apache 2.0 use crate::{ + Client, config::Config, error::OpenAIError, types::{ ChatCompletionResponseStream, CreateChatCompletionRequest, CreateChatCompletionResponse, }, - Client, }; /// Given a list of messages comprising a conversation, the model will return a response. diff --git a/lib/async-openai/src/client.rs b/lib/async-openai/src/client.rs index ce1c2f0eb7..b0f118e8be 100644 --- a/lib/async-openai/src/client.rs +++ b/lib/async-openai/src/client.rs @@ -11,20 +11,20 @@ use std::pin::Pin; use bytes::Bytes; -use futures::{stream::StreamExt, Stream}; +use futures::{Stream, stream::StreamExt}; use reqwest::multipart::Form; use reqwest_eventsource::{Event, EventSource, RequestBuilderExt}; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use crate::{ + Assistants, Audio, AuditLogs, Batches, Chat, Completions, Embeddings, FineTuning, Invites, + Models, Projects, Responses, Threads, Uploads, Users, VectorStores, config::{Config, OpenAIConfig}, - error::{map_deserialization_error, ApiError, OpenAIError, WrappedError}, + error::{ApiError, OpenAIError, WrappedError, map_deserialization_error}, file::Files, image::Images, moderation::Moderations, traits::AsyncTryFrom, - Assistants, Audio, AuditLogs, Batches, Chat, Completions, Embeddings, FineTuning, Invites, - Models, Projects, Responses, Threads, Uploads, Users, VectorStores, }; #[derive(Debug, Clone, Default)] diff --git a/lib/async-openai/src/config.rs b/lib/async-openai/src/config.rs index e407be7ede..dcae815713 100644 --- a/lib/async-openai/src/config.rs +++ b/lib/async-openai/src/config.rs @@ -9,7 +9,7 @@ // Licensed under Apache 2.0 //! Client configurations: [OpenAIConfig] for OpenAI, [AzureConfig] for Azure OpenAI Service. -use reqwest::header::{HeaderMap, AUTHORIZATION}; +use reqwest::header::{AUTHORIZATION, HeaderMap}; use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; @@ -251,10 +251,10 @@ impl Config for AzureConfig { #[cfg(test)] mod test { use super::*; + use crate::Client; use crate::types::{ ChatCompletionRequestMessage, ChatCompletionRequestUserMessage, CreateChatCompletionRequest, }; - use crate::Client; use std::sync::Arc; #[test] fn test_client_creation() { diff --git a/lib/async-openai/src/download.rs b/lib/async-openai/src/download.rs index d190808ab4..d9f5ab2435 100644 --- a/lib/async-openai/src/download.rs +++ b/lib/async-openai/src/download.rs @@ -10,8 +10,8 @@ use std::path::{Path, PathBuf}; -use base64::{engine::general_purpose, Engine as _}; -use rand::{distr::Alphanumeric, Rng}; +use base64::{Engine as _, engine::general_purpose}; +use rand::{Rng, distr::Alphanumeric}; use reqwest::Url; use crate::error::OpenAIError; diff --git a/lib/async-openai/src/embedding.rs b/lib/async-openai/src/embedding.rs index 450dc586f6..80308bb4ea 100644 --- a/lib/async-openai/src/embedding.rs +++ b/lib/async-openai/src/embedding.rs @@ -9,10 +9,10 @@ // Licensed under Apache 2.0 use crate::{ + Client, config::Config, error::OpenAIError, types::{CreateBase64EmbeddingResponse, CreateEmbeddingRequest, CreateEmbeddingResponse}, - Client, }; #[cfg(not(feature = "byot"))] diff --git a/lib/async-openai/src/file.rs b/lib/async-openai/src/file.rs index d6dd621da0..0bc783602e 100644 --- a/lib/async-openai/src/file.rs +++ b/lib/async-openai/src/file.rs @@ -12,10 +12,10 @@ use bytes::Bytes; use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{CreateFileRequest, DeleteFileResponse, ListFilesResponse, OpenAIFile}, - Client, }; /// Files are used to upload documents that can be used with features like Assistants and Fine-tuning. diff --git a/lib/async-openai/src/fine_tuning.rs b/lib/async-openai/src/fine_tuning.rs index 1e9e43e2d8..3a0726a3fd 100644 --- a/lib/async-openai/src/fine_tuning.rs +++ b/lib/async-openai/src/fine_tuning.rs @@ -11,13 +11,13 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ CreateFineTuningJobRequest, FineTuningJob, ListFineTuningJobCheckpointsResponse, ListFineTuningJobEventsResponse, ListPaginatedFineTuningJobsResponse, }, - Client, }; /// Manage fine-tuning jobs to tailor a model to your specific training data. diff --git a/lib/async-openai/src/image.rs b/lib/async-openai/src/image.rs index b51ea979a6..65a68796a1 100644 --- a/lib/async-openai/src/image.rs +++ b/lib/async-openai/src/image.rs @@ -9,12 +9,12 @@ // Licensed under Apache 2.0 use crate::{ + Client, config::Config, error::OpenAIError, types::{ CreateImageEditRequest, CreateImageRequest, CreateImageVariationRequest, ImagesResponse, }, - Client, }; /// Given a prompt and/or an input image, the model will generate a new image. diff --git a/lib/async-openai/src/invites.rs b/lib/async-openai/src/invites.rs index 02223edbde..c3acf55e7e 100644 --- a/lib/async-openai/src/invites.rs +++ b/lib/async-openai/src/invites.rs @@ -11,10 +11,10 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{Invite, InviteDeleteResponse, InviteListResponse, InviteRequest}, - Client, }; /// Invite and manage invitations for an organization. Invited users are automatically added to the Default project. diff --git a/lib/async-openai/src/messages.rs b/lib/async-openai/src/messages.rs index 84ee76b018..0f1638c666 100644 --- a/lib/async-openai/src/messages.rs +++ b/lib/async-openai/src/messages.rs @@ -11,13 +11,13 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ CreateMessageRequest, DeleteMessageResponse, ListMessagesResponse, MessageObject, ModifyMessageRequest, }, - Client, }; /// Represents a message within a [thread](https://platform.openai.com/docs/api-reference/threads). diff --git a/lib/async-openai/src/model.rs b/lib/async-openai/src/model.rs index d99b7fa63f..6db4a48f6c 100644 --- a/lib/async-openai/src/model.rs +++ b/lib/async-openai/src/model.rs @@ -9,10 +9,10 @@ // Licensed under Apache 2.0 use crate::{ + Client, config::Config, error::OpenAIError, types::{DeleteModelResponse, ListModelResponse, Model}, - Client, }; /// List and describe the various models available in the API. diff --git a/lib/async-openai/src/moderation.rs b/lib/async-openai/src/moderation.rs index f83f2380b5..713822c859 100644 --- a/lib/async-openai/src/moderation.rs +++ b/lib/async-openai/src/moderation.rs @@ -9,10 +9,10 @@ // Licensed under Apache 2.0 use crate::{ + Client, config::Config, error::OpenAIError, types::{CreateModerationRequest, CreateModerationResponse}, - Client, }; /// Given text and/or image inputs, classifies if those inputs are potentially harmful across several categories. diff --git a/lib/async-openai/src/project_api_keys.rs b/lib/async-openai/src/project_api_keys.rs index 2cc9428acd..46f57b1116 100644 --- a/lib/async-openai/src/project_api_keys.rs +++ b/lib/async-openai/src/project_api_keys.rs @@ -11,10 +11,10 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ProjectApiKey, ProjectApiKeyDeleteResponse, ProjectApiKeyListResponse}, - Client, }; /// Manage API keys for a given project. Supports listing and deleting keys for users. diff --git a/lib/async-openai/src/project_service_accounts.rs b/lib/async-openai/src/project_service_accounts.rs index d0994fd477..7bca0f3e62 100644 --- a/lib/async-openai/src/project_service_accounts.rs +++ b/lib/async-openai/src/project_service_accounts.rs @@ -11,6 +11,7 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ @@ -18,7 +19,6 @@ use crate::{ ProjectServiceAccountCreateResponse, ProjectServiceAccountDeleteResponse, ProjectServiceAccountListResponse, }, - Client, }; /// Manage service accounts within a project. A service account is a bot user that is not diff --git a/lib/async-openai/src/project_users.rs b/lib/async-openai/src/project_users.rs index 65f8c0eb2d..92e21ef95b 100644 --- a/lib/async-openai/src/project_users.rs +++ b/lib/async-openai/src/project_users.rs @@ -11,13 +11,13 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ ProjectUser, ProjectUserCreateRequest, ProjectUserDeleteResponse, ProjectUserListResponse, ProjectUserUpdateRequest, }, - Client, }; /// Manage users within a project, including adding, updating roles, and removing users. diff --git a/lib/async-openai/src/projects.rs b/lib/async-openai/src/projects.rs index 4c06988720..febfbc27eb 100644 --- a/lib/async-openai/src/projects.rs +++ b/lib/async-openai/src/projects.rs @@ -11,11 +11,11 @@ use serde::Serialize; use crate::{ + Client, ProjectServiceAccounts, ProjectUsers, config::Config, error::OpenAIError, project_api_keys::ProjectAPIKeys, types::{Project, ProjectCreateRequest, ProjectListResponse, ProjectUpdateRequest}, - Client, ProjectServiceAccounts, ProjectUsers, }; /// Manage the projects within an organization includes creation, updating, and archiving or projects. diff --git a/lib/async-openai/src/responses.rs b/lib/async-openai/src/responses.rs index 7aa7918964..bcd471c4c6 100644 --- a/lib/async-openai/src/responses.rs +++ b/lib/async-openai/src/responses.rs @@ -9,10 +9,10 @@ // Licensed under Apache 2.0 use crate::{ + Client, config::Config, error::OpenAIError, types::responses::{CreateResponse, Response, ResponseStream}, - Client, }; /// Given text input or a list of context items, the model will generate a response. diff --git a/lib/async-openai/src/runs.rs b/lib/async-openai/src/runs.rs index 9aa8ffe57c..c554c067fb 100644 --- a/lib/async-openai/src/runs.rs +++ b/lib/async-openai/src/runs.rs @@ -11,6 +11,7 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, steps::Steps, @@ -18,7 +19,6 @@ use crate::{ AssistantEventStream, CreateRunRequest, ListRunsResponse, ModifyRunRequest, RunObject, SubmitToolOutputsRunRequest, }, - Client, }; /// Represents an execution run on a thread. diff --git a/lib/async-openai/src/steps.rs b/lib/async-openai/src/steps.rs index 30c5b7a09a..85b5d192de 100644 --- a/lib/async-openai/src/steps.rs +++ b/lib/async-openai/src/steps.rs @@ -11,10 +11,10 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ListRunStepsResponse, RunStepObject}, - Client, }; /// Represents a step in execution of a run. diff --git a/lib/async-openai/src/threads.rs b/lib/async-openai/src/threads.rs index a15c31213e..1c4ffab228 100644 --- a/lib/async-openai/src/threads.rs +++ b/lib/async-openai/src/threads.rs @@ -9,13 +9,13 @@ // Licensed under Apache 2.0 use crate::{ + Client, Messages, Runs, config::Config, error::OpenAIError, types::{ AssistantEventStream, CreateThreadAndRunRequest, CreateThreadRequest, DeleteThreadResponse, ModifyThreadRequest, RunObject, ThreadObject, }, - Client, Messages, Runs, }; /// Create threads that assistants can interact with. diff --git a/lib/async-openai/src/types/assistant_stream.rs b/lib/async-openai/src/types/assistant_stream.rs index e9a305aa51..2894bd42a4 100644 --- a/lib/async-openai/src/types/assistant_stream.rs +++ b/lib/async-openai/src/types/assistant_stream.rs @@ -13,7 +13,7 @@ use std::pin::Pin; use futures::Stream; use serde::Deserialize; -use crate::error::{map_deserialization_error, ApiError, OpenAIError}; +use crate::error::{ApiError, OpenAIError, map_deserialization_error}; use super::{ MessageDeltaObject, MessageObject, RunObject, RunStepDeltaObject, RunStepObject, ThreadObject, diff --git a/lib/async-openai/src/types/embedding.rs b/lib/async-openai/src/types/embedding.rs index bd89a09534..63afa17560 100644 --- a/lib/async-openai/src/types/embedding.rs +++ b/lib/async-openai/src/types/embedding.rs @@ -8,7 +8,7 @@ // Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. // Licensed under Apache 2.0 -use base64::engine::{general_purpose, Engine}; +use base64::engine::{Engine, general_purpose}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; diff --git a/lib/async-openai/src/types/impls.rs b/lib/async-openai/src/types/impls.rs index ccb8a98ffc..6b2feeaa0b 100644 --- a/lib/async-openai/src/types/impls.rs +++ b/lib/async-openai/src/types/impls.rs @@ -24,7 +24,6 @@ use crate::{ use bytes::Bytes; use super::{ - responses::{CodeInterpreterContainer, Input, InputContent, Role as ResponsesRole}, AddUploadPartRequest, AudioInput, AudioResponseFormat, ChatCompletionFunctionCall, ChatCompletionFunctions, ChatCompletionNamedToolChoice, ChatCompletionRequestAssistantMessage, ChatCompletionRequestAssistantMessageContent, ChatCompletionRequestDeveloperMessage, @@ -40,6 +39,7 @@ use super::{ EmbeddingInput, FileInput, FilePurpose, FunctionName, Image, ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse, ModerationInput, Prompt, Role, Stop, TimestampGranularity, + responses::{CodeInterpreterContainer, Input, InputContent, Role as ResponsesRole}, }; /// for `impl_from!(T, Enum)`, implements diff --git a/lib/async-openai/src/uploads.rs b/lib/async-openai/src/uploads.rs index 627d9f682a..290e368a7c 100644 --- a/lib/async-openai/src/uploads.rs +++ b/lib/async-openai/src/uploads.rs @@ -9,10 +9,10 @@ // Licensed under Apache 2.0 use crate::{ + Client, config::Config, error::OpenAIError, types::{AddUploadPartRequest, CompleteUploadRequest, CreateUploadRequest, Upload, UploadPart}, - Client, }; /// Allows you to upload large files in multiple parts. diff --git a/lib/async-openai/src/users.rs b/lib/async-openai/src/users.rs index 67566cf396..202679d045 100644 --- a/lib/async-openai/src/users.rs +++ b/lib/async-openai/src/users.rs @@ -11,10 +11,10 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{User, UserDeleteResponse, UserListResponse, UserRoleUpdateRequest}, - Client, }; /// Manage users and their role in an organization. Users will be automatically added to the Default project. diff --git a/lib/async-openai/src/util.rs b/lib/async-openai/src/util.rs index dbd202d0e7..88a28e5952 100644 --- a/lib/async-openai/src/util.rs +++ b/lib/async-openai/src/util.rs @@ -29,7 +29,7 @@ pub(crate) async fn file_stream_body(source: InputSource) -> Result { return Err(OpenAIError::FileReadError( "Cannot create stream from non-file source".to_string(), - )) + )); } }; Ok(body) diff --git a/lib/async-openai/src/vector_store_file_batches.rs b/lib/async-openai/src/vector_store_file_batches.rs index b4068fb68e..7752cb104e 100644 --- a/lib/async-openai/src/vector_store_file_batches.rs +++ b/lib/async-openai/src/vector_store_file_batches.rs @@ -11,12 +11,12 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ CreateVectorStoreFileBatchRequest, ListVectorStoreFilesResponse, VectorStoreFileBatchObject, }, - Client, }; /// Vector store file batches represent operations to add multiple files to a vector store. diff --git a/lib/async-openai/src/vector_store_files.rs b/lib/async-openai/src/vector_store_files.rs index da2945e8fe..04890a13f0 100644 --- a/lib/async-openai/src/vector_store_files.rs +++ b/lib/async-openai/src/vector_store_files.rs @@ -11,13 +11,13 @@ use serde::Serialize; use crate::{ + Client, config::Config, error::OpenAIError, types::{ CreateVectorStoreFileRequest, DeleteVectorStoreFileResponse, ListVectorStoreFilesResponse, VectorStoreFileContentResponse, VectorStoreFileObject, }, - Client, }; /// Vector store files represent files inside a vector store. diff --git a/lib/async-openai/src/vector_stores.rs b/lib/async-openai/src/vector_stores.rs index 7e7edf8d63..8350b538fb 100644 --- a/lib/async-openai/src/vector_stores.rs +++ b/lib/async-openai/src/vector_stores.rs @@ -11,6 +11,7 @@ use serde::Serialize; use crate::{ + Client, VectorStoreFiles, config::Config, error::OpenAIError, types::{ @@ -19,7 +20,6 @@ use crate::{ VectorStoreSearchResultsPage, }, vector_store_file_batches::VectorStoreFileBatches, - Client, VectorStoreFiles, }; pub struct VectorStores<'c, C: Config> { diff --git a/lib/async-openai/tests/bring-your-own-type.rs b/lib/async-openai/tests/bring-your-own-type.rs index 6c084af7f7..2075f90ea6 100644 --- a/lib/async-openai/tests/bring-your-own-type.rs +++ b/lib/async-openai/tests/bring-your-own-type.rs @@ -12,9 +12,9 @@ //! The purpose of this test to make sure that all _byot methods compiles with custom types. use std::pin::Pin; -use dynamo_async_openai::{error::OpenAIError, Client}; +use dynamo_async_openai::{Client, error::OpenAIError}; use futures::Stream; -use serde_json::{json, Value}; +use serde_json::{Value, json}; impl dynamo_async_openai::traits::AsyncTryFrom for reqwest::multipart::Form { type Error = OpenAIError; diff --git a/lib/async-openai/tests/whisper.rs b/lib/async-openai/tests/whisper.rs index 8f332a97bd..0fa9fad6a3 100644 --- a/lib/async-openai/tests/whisper.rs +++ b/lib/async-openai/tests/whisper.rs @@ -9,7 +9,7 @@ // Licensed under Apache 2.0 use dynamo_async_openai::types::CreateTranslationRequestArgs; -use dynamo_async_openai::{types::CreateTranscriptionRequestArgs, Client}; +use dynamo_async_openai::{Client, types::CreateTranscriptionRequestArgs}; use tokio_test::assert_err; #[tokio::test] diff --git a/lib/bindings/c/src/lib.rs b/lib/bindings/c/src/lib.rs index 62c22d6325..eafe1d94e6 100644 --- a/lib/bindings/c/src/lib.rs +++ b/lib/bindings/c/src/lib.rs @@ -48,7 +48,7 @@ pub enum DynamoLlmResult { /// # Safety /// the namespace_c_str and component_c_str are passed as pointers to C strings -#[no_mangle] +#[unsafe(no_mangle)] pub unsafe extern "C" fn dynamo_llm_init( namespace_c_str: *const c_char, component_c_str: *const c_char, @@ -108,7 +108,7 @@ pub unsafe extern "C" fn dynamo_llm_init( } } -#[no_mangle] +#[unsafe(no_mangle)] pub extern "C" fn dynamo_llm_shutdown() -> DynamoLlmResult { let wk = match WK.get() { Some(wk) => wk, @@ -123,7 +123,7 @@ pub extern "C" fn dynamo_llm_shutdown() -> DynamoLlmResult { DynamoLlmResult::OK } -#[no_mangle] +#[unsafe(no_mangle)] pub extern "C" fn dynamo_llm_load_publisher_create() -> DynamoLlmResult { DynamoLlmResult::OK } @@ -191,11 +191,7 @@ fn kv_event_create_stored_from_parts( if num_toks != (kv_block_size as usize) { if WARN_COUNT .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| { - if c < 3 { - Some(c + 1) - } else { - None - } + if c < 3 { Some(c + 1) } else { None } }) .is_ok() { @@ -256,7 +252,7 @@ pub struct DynamoKvStoredEventParams { /// # Safety /// parent_hash is passed as pointer to indicate whether the blocks /// has a parent hash or not. nullptr is used to represent no parent hash -#[no_mangle] +#[unsafe(no_mangle)] pub unsafe extern "C" fn dynamo_kv_event_publish_stored( event_id: u64, token_ids: *const u32, @@ -293,7 +289,7 @@ pub unsafe extern "C" fn dynamo_kv_event_publish_stored( } } -#[no_mangle] +#[unsafe(no_mangle)] pub extern "C" fn dynamo_kv_event_publish_removed( event_id: u64, block_ids: *const u64, diff --git a/lib/engines/llamacpp/src/lib.rs b/lib/engines/llamacpp/src/lib.rs index 3921e455ae..6dd7f586d3 100644 --- a/lib/engines/llamacpp/src/lib.rs +++ b/lib/engines/llamacpp/src/lib.rs @@ -10,17 +10,17 @@ use std::{ use async_stream::stream; use dynamo_runtime::engine::{AsyncEngine, AsyncEngineContextProvider, ResponseStream}; use dynamo_runtime::pipeline::error as pipeline_error; -use dynamo_runtime::pipeline::{async_trait, Error, ManyOut, SingleIn}; +use dynamo_runtime::pipeline::{Error, ManyOut, SingleIn, async_trait}; use dynamo_runtime::protocols::annotated::Annotated; use dynamo_runtime::{CancellationToken, ErrorContext, Result}; use llama_cpp_2::{ - context::{params::LlamaContextParams, LlamaContext}, + LogOptions, + context::{LlamaContext, params::LlamaContextParams}, llama_backend::LlamaBackend, llama_batch::LlamaBatch, - model::{params::LlamaModelParams, LlamaModel}, + model::{LlamaModel, params::LlamaModelParams}, sampling::LlamaSampler, token::LlamaToken, - LogOptions, }; use dynamo_llm::protocols::common::llm_backend::LLMEngineOutput; diff --git a/lib/engines/mistralrs/src/lib.rs b/lib/engines/mistralrs/src/lib.rs index 7a7f4a59b2..ad819e648d 100644 --- a/lib/engines/mistralrs/src/lib.rs +++ b/lib/engines/mistralrs/src/lib.rs @@ -25,7 +25,7 @@ use dynamo_runtime::protocols::annotated::Annotated; use dynamo_llm::protocols::openai::{ chat_completions::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse}, - completions::{prompt_to_string, NvCreateCompletionRequest, NvCreateCompletionResponse}, + completions::{NvCreateCompletionRequest, NvCreateCompletionResponse, prompt_to_string}, embeddings::{NvCreateEmbeddingRequest, NvCreateEmbeddingResponse}, }; @@ -240,17 +240,16 @@ impl MistralRsEngine { })); // Send warmup request and consume response - if let Ok(sender) = engine.mistralrs.get_sender(None) { - if let Ok(()) = sender.send(warmup_request).await { - if let Some(response) = rx.recv().await { - match response.as_result() { - Ok(r) => { - tracing::debug!(request_id, "Warmup response: {r:?}"); - } - Err(err) => { - tracing::error!(request_id, %err, "Failed converting response to result."); - } - } + if let Ok(sender) = engine.mistralrs.get_sender(None) + && let Ok(()) = sender.send(warmup_request).await + && let Some(response) = rx.recv().await + { + match response.as_result() { + Ok(r) => { + tracing::debug!(request_id, "Warmup response: {r:?}"); + } + Err(err) => { + tracing::error!(request_id, %err, "Failed converting response to result."); } } } diff --git a/lib/llm/benches/tokenizer.rs b/lib/llm/benches/tokenizer.rs index 8c2c793245..743f500af1 100644 --- a/lib/llm/benches/tokenizer.rs +++ b/lib/llm/benches/tokenizer.rs @@ -4,13 +4,13 @@ use std::hint::black_box; use std::sync::Arc; -use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use dynamo_llm::backend::Decoder; use dynamo_llm::protocols::common::StopConditions; +use dynamo_llm::tokenizers::DecodeStream; use dynamo_llm::tokenizers::hf::HuggingFaceTokenizer; use dynamo_llm::tokenizers::traits::{Encoder, Tokenizer}; -use dynamo_llm::tokenizers::DecodeStream; use dynamo_llm::types::TokenIdType; const TEST_TOKENIZER: &str = concat!( diff --git a/lib/llm/src/backend.rs b/lib/llm/src/backend.rs index 6cf98a92bf..aa4f4ef982 100644 --- a/lib/llm/src/backend.rs +++ b/lib/llm/src/backend.rs @@ -24,22 +24,22 @@ use tracing as log; use crate::model_card::{ModelDeploymentCard, TokenizerKind}; use dynamo_runtime::{ pipeline::{ - async_trait, AsyncEngineContextProvider, ManyOut, Operator, ResponseStream, - ServerStreamingEngine, SingleIn, + AsyncEngineContextProvider, ManyOut, Operator, ResponseStream, ServerStreamingEngine, + SingleIn, async_trait, }, protocols::annotated::Annotated, }; use crate::protocols::{ + TokenIdType, common::{ + StopConditions, llm_backend::{ BackendOutput, EmbeddingsEngineOutput, FinishReason, LLMEngineOutput, PreprocessedRequest, }, preprocessor::PreprocessedEmbeddingRequest, - StopConditions, }, - TokenIdType, }; use crate::tokenizers::{DecodeStream, HuggingFaceTokenizer, Tokenizer}; use tokenizers::Tokenizer as HfTokenizer; @@ -149,10 +149,11 @@ impl } // if we have a data field without an event, then we might need to update the data - if let Some(data) = &output.data { - if data.text.is_some() && !state.validate_engine_decode { - return Some((output, state)); - } + if let Some(data) = &output.data + && data.text.is_some() + && !state.validate_engine_decode + { + return Some((output, state)); } let data = output.data.as_ref().unwrap(); @@ -425,45 +426,44 @@ impl Decoder { // check stop sequences - the jail will always hold at least the largest stop sequence // if jail_max_bytes is 0, then there are no stop sequences - if self.jail_max_bytes > 0 { - if let Some(token) = &token { - let pre_append = self.jail.len(); - log::debug!("pre_append: {}", pre_append); - log::debug!("jail: {}", self.jail); - self.jail.push_str(token); - log::debug!("post_append: {}", self.jail.len()); - log::debug!("jail: {}", self.jail); - - for seq in &self.hidden_stop_sequences { - log::debug!("stop seq: {}", seq); - if let Some(offset) = - galil_seiferas::gs_find(self.jail.as_bytes(), seq.as_bytes()) - { - log::debug!("offset: {}", offset); - // return only new bytes after pre_append .. offset+seq.len() - // example: seq = "ox", token = "boxes", return "b" - // note: this changes when we start jailing tokens for partial matches - // on the suffix of the jail with prefixes of the stop sequences - // - // we might have returned a partial match, if so, then offset < pre_append - // in that case, we return the empty string - let partial_token = if offset >= pre_append { - self.jail[pre_append..offset].to_string() - } else { - "".to_string() - }; - return Ok(StepResult::with_stop_trigger( - Some(partial_token), - StopTrigger::HiddenStopSequenceDetected(seq.to_string()), - )); - } + if self.jail_max_bytes > 0 + && let Some(token) = &token + { + let pre_append = self.jail.len(); + log::debug!("pre_append: {}", pre_append); + log::debug!("jail: {}", self.jail); + self.jail.push_str(token); + log::debug!("post_append: {}", self.jail.len()); + log::debug!("jail: {}", self.jail); + + for seq in &self.hidden_stop_sequences { + log::debug!("stop seq: {}", seq); + if let Some(offset) = galil_seiferas::gs_find(self.jail.as_bytes(), seq.as_bytes()) + { + log::debug!("offset: {}", offset); + // return only new bytes after pre_append .. offset+seq.len() + // example: seq = "ox", token = "boxes", return "b" + // note: this changes when we start jailing tokens for partial matches + // on the suffix of the jail with prefixes of the stop sequences + // + // we might have returned a partial match, if so, then offset < pre_append + // in that case, we return the empty string + let partial_token = if offset >= pre_append { + self.jail[pre_append..offset].to_string() + } else { + "".to_string() + }; + return Ok(StepResult::with_stop_trigger( + Some(partial_token), + StopTrigger::HiddenStopSequenceDetected(seq.to_string()), + )); } + } - if self.jail.len() > self.jail_max_bytes { - // truncate the jail - let drain_len = self.jail.len() - self.jail_max_bytes; - self.jail.drain(0..drain_len); - } + if self.jail.len() > self.jail_max_bytes { + // truncate the jail + let drain_len = self.jail.len() - self.jail_max_bytes; + self.jail.drain(0..drain_len); } } @@ -485,11 +485,9 @@ impl Decoder { .map(|x| x.should_hide_text()) .unwrap_or(false); - if !hide_text { - if let Some(token) = &token { - text.get_or_insert_with(|| String::with_capacity(token_ids.len())) - .push_str(token); - } + if !hide_text && let Some(token) = &token { + text.get_or_insert_with(|| String::with_capacity(token_ids.len())) + .push_str(token); } tokens.push(token); diff --git a/lib/llm/src/block_manager.rs b/lib/llm/src/block_manager.rs index 9f1f2566d4..b4a797de6f 100644 --- a/lib/llm/src/block_manager.rs +++ b/lib/llm/src/block_manager.rs @@ -38,18 +38,18 @@ pub mod controller; pub use crate::common::dtype::DType; pub use block::{ + BasicMetadata, BlockMetadata, Blocks, ImmutableBlock, MutableBlock, locality::{self, LocalityProvider, LogicalResources}, nixl::{BlockDescriptorList, IsImmutable, IsMutable, MutabilityKind, RemoteBlock}, - BasicMetadata, BlockMetadata, Blocks, ImmutableBlock, MutableBlock, }; pub use config::*; -pub use layout::{nixl::NixlLayout, LayoutConfig, LayoutConfigBuilder, LayoutError, LayoutType}; +pub use layout::{LayoutConfig, LayoutConfigBuilder, LayoutError, LayoutType, nixl::NixlLayout}; pub use offload::request::BlockResult; pub use pool::{BlockPool, ManagedBlockPool}; pub use storage::{ - nixl::NixlRegisterableStorage, DeviceStorage, DiskStorage, PinnedStorage, Storage, - StorageAllocator, + DeviceStorage, DiskStorage, PinnedStorage, Storage, StorageAllocator, + nixl::NixlRegisterableStorage, }; pub use tokio_util::sync::CancellationToken; diff --git a/lib/llm/src/block_manager/block.rs b/lib/llm/src/block_manager/block.rs index 693e5dee01..173a5a5ceb 100644 --- a/lib/llm/src/block_manager/block.rs +++ b/lib/llm/src/block_manager/block.rs @@ -21,7 +21,7 @@ pub mod registry; pub mod state; pub mod transfer; -pub use data::{view, BlockData, BlockDataExt, BlockDataProvider, BlockDataProviderMut}; +pub use data::{BlockData, BlockDataExt, BlockDataProvider, BlockDataProviderMut, view}; pub use locality::LocalityProvider; pub use crate::tokens::TokenBlockError; @@ -37,10 +37,10 @@ use crate::block_manager::{ use crate::tokens::{SaltHash, SequenceHash, Token, TokenBlock, Tokens}; use super::{ + WorkerID, events::PublishHandle, layout::{BlockLayout, LayoutError, LayoutType}, storage::StorageType, - WorkerID, }; use derive_getters::Getters; @@ -158,7 +158,7 @@ pub trait AsBlockMutSlice<'a, B: 'a> { pub trait IntoWritableBlocks { type Output: WritableBlocks; fn into_writable_blocks(self, manager: &BlockManager) - -> BlockResult; + -> BlockResult; } impl @@ -176,7 +176,7 @@ impl pub trait IntoReadableBlocks { type Output: ReadableBlocks; fn into_readable_blocks(self, manager: &BlockManager) - -> BlockResult; + -> BlockResult; } impl @@ -657,10 +657,10 @@ impl std::fmt::Debug for Muta impl Drop for MutableBlock { fn drop(&mut self) { tracing::debug!("drop: {:?}", self); - if let Some(block) = self.block.take() { - if self.return_tx.send(block).is_err() { - tracing::warn!("block pool shutdown before block was returned"); - } + if let Some(block) = self.block.take() + && self.return_tx.send(block).is_err() + { + tracing::warn!("block pool shutdown before block was returned"); } } } @@ -957,9 +957,9 @@ pub mod nixl { use super::view::{BlockKind, Kind, LayerKind}; use super::super::{ + WorkerID, layout::nixl::{NixlLayout, SerializedNixlBlockLayout}, storage::nixl::{MemType, NixlRegisterableStorage, NixlStorage}, - WorkerID, }; use derive_getters::{Dissolve, Getters}; @@ -1360,9 +1360,7 @@ pub mod nixl { #[error("Input block list cannot be empty")] EmptyInput, - #[error( - "Blocks in the input list are not homogeneous (worker_id, block_set_idx mismatch)" - )] + #[error("Blocks in the input list are not homogeneous (worker_id, block_set_idx mismatch)")] NotHomogeneous, #[error("Serialization failed: {0}")] diff --git a/lib/llm/src/block_manager/block/data.rs b/lib/llm/src/block_manager/block/data.rs index c8f3c859ce..e84c851066 100644 --- a/lib/llm/src/block_manager/block/data.rs +++ b/lib/llm/src/block_manager/block/data.rs @@ -46,7 +46,11 @@ pub trait BlockDataExt: Send + Sync + 'static + std::fmt::Debug { fn is_local_mut(&mut self) -> Option<&mut dyn BlockDataViews>; /// Get a read-only view of this block's storage for a layer - fn layer_view(&self, layer_idx: usize, outer_idx: usize) -> BlockResult> { + fn layer_view( + &self, + layer_idx: usize, + outer_idx: usize, + ) -> BlockResult> { match self.is_local() { Some(views) => views.local_layer_view(layer_idx, outer_idx), None => Err(BlockError::ViewsNotAvailableOnLogicalBlocks), @@ -58,7 +62,7 @@ pub trait BlockDataExt: Send + Sync + 'static + std::fmt::Debug { &mut self, layer_idx: usize, outer_idx: usize, - ) -> BlockResult> { + ) -> BlockResult> { match self.is_local_mut() { Some(views) => views.local_layer_view_mut(layer_idx, outer_idx), None => Err(BlockError::ViewsNotAvailableOnLogicalBlocks), @@ -66,7 +70,7 @@ pub trait BlockDataExt: Send + Sync + 'static + std::fmt::Debug { } /// Get a read-only view of this block's storage - fn block_view(&self) -> BlockResult> { + fn block_view(&self) -> BlockResult> { match self.is_local() { Some(views) => views.local_block_view(), None => Err(BlockError::ViewsNotAvailableOnLogicalBlocks), @@ -74,7 +78,7 @@ pub trait BlockDataExt: Send + Sync + 'static + std::fmt::Debug { } /// Get a mutable view of this block's storage - fn block_view_mut(&mut self) -> BlockResult> { + fn block_view_mut(&mut self) -> BlockResult> { match self.is_local_mut() { Some(views) => views.local_block_view_mut(), None => Err(BlockError::ViewsNotAvailableOnLogicalBlocks), @@ -88,20 +92,20 @@ pub trait BlockDataViews { &self, layer_idx: usize, outer_idx: usize, - ) -> BlockResult>; + ) -> BlockResult>; /// Get a mutable view of this block's storage for a layer fn local_layer_view_mut( &mut self, layer_idx: usize, outer_idx: usize, - ) -> BlockResult>; + ) -> BlockResult>; /// Get a read-only view of this block's storage - fn local_block_view(&self) -> BlockResult>; + fn local_block_view(&self) -> BlockResult>; /// Get a mutable view of this block's storage - fn local_block_view_mut(&mut self) -> BlockResult>; + fn local_block_view_mut(&mut self) -> BlockResult>; } pub trait BlockDataProvider: StorageTypeProvider { diff --git a/lib/llm/src/block_manager/block/data/local.rs b/lib/llm/src/block_manager/block/data/local.rs index 000016c870..3fd22c3a82 100644 --- a/lib/llm/src/block_manager/block/data/local.rs +++ b/lib/llm/src/block_manager/block/data/local.rs @@ -101,7 +101,7 @@ impl BlockDataViews for LocalBlockData { &self, layer_idx: usize, outer_idx: usize, - ) -> BlockResult> { + ) -> BlockResult> { let mr = self .layout .memory_region(self.block_idx, layer_idx, outer_idx)?; @@ -113,14 +113,14 @@ impl BlockDataViews for LocalBlockData { &mut self, layer_idx: usize, outer_idx: usize, - ) -> BlockResult> { + ) -> BlockResult> { let mr = self .layout .memory_region(self.block_idx, layer_idx, outer_idx)?; unsafe { view::LayerViewMut::new(self, mr.addr(), mr.size(), mr.storage_type()) } } - fn local_block_view(&self) -> BlockResult> { + fn local_block_view(&self) -> BlockResult> { if self.is_fully_contiguous() { let mr = self.layout.memory_region(self.block_idx, 0, 0)?; let offset = mr.addr(); @@ -134,7 +134,7 @@ impl BlockDataViews for LocalBlockData { } } - fn local_block_view_mut(&mut self) -> BlockResult> { + fn local_block_view_mut(&mut self) -> BlockResult> { if self.is_fully_contiguous() { let mr = self.layout.memory_region(self.block_idx, 0, 0)?; let offset = mr.addr(); diff --git a/lib/llm/src/block_manager/block/data/logical.rs b/lib/llm/src/block_manager/block/data/logical.rs index cc0ef841c9..3aee97bc3e 100644 --- a/lib/llm/src/block_manager/block/data/logical.rs +++ b/lib/llm/src/block_manager/block/data/logical.rs @@ -7,8 +7,8 @@ pub mod distributed_leader_worker; pub mod null; use crate::block_manager::block::{ - transfer::{TransferContext, TransferError, WriteToStrategy}, BlockDataProvider, ReadableBlock, WritableBlock, + transfer::{TransferContext, TransferError, WriteToStrategy}, }; use crate::block_manager::locality::Logical; use crate::block_manager::storage::{self, nixl::NixlDescriptor}; diff --git a/lib/llm/src/block_manager/block/locality.rs b/lib/llm/src/block_manager/block/locality.rs index 71253ad991..4d8492da42 100644 --- a/lib/llm/src/block_manager/block/locality.rs +++ b/lib/llm/src/block_manager/block/locality.rs @@ -16,7 +16,7 @@ use super::*; use crate::block_manager::block::transfer::{ - handle_local_transfer, TransferContext, TransferError, WriteToStrategy, + TransferContext, TransferError, WriteToStrategy, handle_local_transfer, }; use crate::block_manager::storage::{self, nixl::NixlDescriptor}; diff --git a/lib/llm/src/block_manager/block/registry.rs b/lib/llm/src/block_manager/block/registry.rs index de3b00cf4f..f794d6431c 100644 --- a/lib/llm/src/block_manager/block/registry.rs +++ b/lib/llm/src/block_manager/block/registry.rs @@ -109,19 +109,19 @@ impl BlockRegistry { { let mut blocks = blocks.lock().unwrap(); - if let Some(handle) = blocks.get(&sequence_hash) { - if handle.upgrade().is_none() { - blocks.remove(&sequence_hash); - } + if let Some(handle) = blocks.get(&sequence_hash) + && handle.upgrade().is_none() + { + blocks.remove(&sequence_hash); } } let mut global_registry = global_registry.lock().unwrap(); - if let Some(entry) = global_registry.get(&sequence_hash) { - if entry.upgrade().is_none() { - global_registry.remove(&sequence_hash); - } + if let Some(entry) = global_registry.get(&sequence_hash) + && entry.upgrade().is_none() + { + global_registry.remove(&sequence_hash); } } }); @@ -136,10 +136,10 @@ impl BlockRegistry { pub fn is_registered(&self, sequence_hash: SequenceHash) -> bool { let blocks = self.blocks.lock().unwrap(); - if let Some(handle) = blocks.get(&sequence_hash) { - if let Some(_handle) = handle.upgrade() { - return true; - } + if let Some(handle) = blocks.get(&sequence_hash) + && let Some(_handle) = handle.upgrade() + { + return true; } false } @@ -161,12 +161,12 @@ impl BlockRegistry { let mut blocks = self.blocks.lock().unwrap(); // If an identical block already exists in this pool, return an error. - if let Some(handle) = blocks.get(&sequence_hash) { - if let Some(_handle) = handle.upgrade() { - return Err(BlockRegistrationError::BlockAlreadyRegistered( - sequence_hash, - )); - } + if let Some(handle) = blocks.get(&sequence_hash) + && let Some(_handle) = handle.upgrade() + { + return Err(BlockRegistrationError::BlockAlreadyRegistered( + sequence_hash, + )); } let mut publish_handle = None; @@ -179,10 +179,10 @@ impl BlockRegistry { let mut global_registry = self.global_registry.lock().unwrap(); // If an identical block exists in other pool, use the same registration handle. - if let Some(handle) = global_registry.get(&sequence_hash) { - if let Some(handle) = handle.upgrade() { - break 'reg_block handle; - } + if let Some(handle) = global_registry.get(&sequence_hash) + && let Some(handle) = handle.upgrade() + { + break 'reg_block handle; } // Otherwise, create a new registration handle. diff --git a/lib/llm/src/block_manager/block/state.rs b/lib/llm/src/block_manager/block/state.rs index dbb1965e82..a5d35835e7 100644 --- a/lib/llm/src/block_manager/block/state.rs +++ b/lib/llm/src/block_manager/block/state.rs @@ -17,8 +17,8 @@ use std::sync::Arc; use derive_getters::Getters; -use super::registry::{BlockHandle, RegistrationHandle}; use super::Result; +use super::registry::{BlockHandle, RegistrationHandle}; use crate::tokens::{PartialTokenBlock, SaltHash, Token, TokenBlock, Tokens}; #[derive(Debug, thiserror::Error)] diff --git a/lib/llm/src/block_manager/block/transfer.rs b/lib/llm/src/block_manager/block/transfer.rs index 91afc9c30b..0b958fbe28 100644 --- a/lib/llm/src/block_manager/block/transfer.rs +++ b/lib/llm/src/block_manager/block/transfer.rs @@ -22,8 +22,8 @@ mod strategy; use super::*; use crate::block_manager::storage::{ - nixl::{NixlRegisterableStorage, NixlStorage}, DeviceStorage, DiskStorage, PinnedStorage, SystemStorage, + nixl::{NixlRegisterableStorage, NixlStorage}, }; use cudarc::driver::CudaStream; diff --git a/lib/llm/src/block_manager/block/transfer/context.rs b/lib/llm/src/block_manager/block/transfer/context.rs index 692c3320ef..72258adb33 100644 --- a/lib/llm/src/block_manager/block/transfer/context.rs +++ b/lib/llm/src/block_manager/block/transfer/context.rs @@ -15,7 +15,7 @@ use super::*; -use cudarc::driver::{sys::CUevent_flags, CudaEvent, CudaStream}; +use cudarc::driver::{CudaEvent, CudaStream, sys::CUevent_flags}; use nixl_sys::Agent as NixlAgent; use std::sync::Arc; @@ -107,10 +107,10 @@ impl TransferContext { impl Drop for TransferContext { fn drop(&mut self) { self.cancel_token.cancel(); - if let Some(handle) = self.cuda_event_worker.take() { - if let Err(e) = handle.join() { - tracing::error!("Error joining CUDA event worker: {:?}", e); - } + if let Some(handle) = self.cuda_event_worker.take() + && let Err(e) = handle.join() + { + tracing::error!("Error joining CUDA event worker: {:?}", e); } } } diff --git a/lib/llm/src/block_manager/block/transfer/cuda.rs b/lib/llm/src/block_manager/block/transfer/cuda.rs index 3a9e92a7c0..25c0217586 100644 --- a/lib/llm/src/block_manager/block/transfer/cuda.rs +++ b/lib/llm/src/block_manager/block/transfer/cuda.rs @@ -177,9 +177,11 @@ unsafe fn cuda_memcpy_h2d( "Source and destination device memory regions must not overlap for D2D copy" ); - let src_slice = std::slice::from_raw_parts(src_ptr, size); - cuda_result::memcpy_htod_async(dst_ptr as u64, src_slice, stream.cu_stream()) - .map_err(|e| TransferError::ExecutionError(format!("CUDA H2D memcpy failed: {}", e)))?; + unsafe { + let src_slice = std::slice::from_raw_parts(src_ptr, size); + cuda_result::memcpy_htod_async(dst_ptr as u64, src_slice, stream.cu_stream()) + .map_err(|e| TransferError::ExecutionError(format!("CUDA H2D memcpy failed: {}", e)))? + }; Ok(()) } @@ -199,9 +201,11 @@ unsafe fn cuda_memcpy_d2h( "Source and destination device memory regions must not overlap for D2D copy" ); - let dst_slice = std::slice::from_raw_parts_mut(dst_ptr, size); - cuda_result::memcpy_dtoh_async(dst_slice, src_ptr as u64, stream.cu_stream()) - .map_err(|e| TransferError::ExecutionError(format!("CUDA D2H memcpy failed: {}", e)))?; + unsafe { + let dst_slice = std::slice::from_raw_parts_mut(dst_ptr, size); + cuda_result::memcpy_dtoh_async(dst_slice, src_ptr as u64, stream.cu_stream()) + .map_err(|e| TransferError::ExecutionError(format!("CUDA D2H memcpy failed: {}", e)))?; + } Ok(()) } @@ -221,8 +225,10 @@ unsafe fn cuda_memcpy_d2d( "Source and destination device memory regions must not overlap for D2D copy" ); - cuda_result::memcpy_dtod_async(dst_ptr as u64, src_ptr as u64, size, stream.cu_stream()) - .map_err(|e| TransferError::ExecutionError(format!("CUDA D2D memcpy failed: {}", e)))?; + unsafe { + cuda_result::memcpy_dtod_async(dst_ptr as u64, src_ptr as u64, size, stream.cu_stream()) + .map_err(|e| TransferError::ExecutionError(format!("CUDA D2D memcpy failed: {}", e)))? + }; Ok(()) } diff --git a/lib/llm/src/block_manager/block/transfer/memcpy.rs b/lib/llm/src/block_manager/block/transfer/memcpy.rs index da847d28e5..969240f5cc 100644 --- a/lib/llm/src/block_manager/block/transfer/memcpy.rs +++ b/lib/llm/src/block_manager/block/transfer/memcpy.rs @@ -78,5 +78,5 @@ unsafe fn memcpy(src_ptr: *const u8, dst_ptr: *mut u8, size: usize) { "Source and destination memory regions must not overlap for copy_nonoverlapping" ); - std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, size); + unsafe { std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, size) }; } diff --git a/lib/llm/src/block_manager/connector/protocol.rs b/lib/llm/src/block_manager/connector/protocol.rs index 58bac0ea02..71e027672c 100644 --- a/lib/llm/src/block_manager/connector/protocol.rs +++ b/lib/llm/src/block_manager/connector/protocol.rs @@ -53,7 +53,7 @@ //! //! [`SchedulerOutput`] is transform -use super::scheduler::{SchedulingDecision, DISCONNECTED_WARNING}; +use super::scheduler::{DISCONNECTED_WARNING, SchedulingDecision}; use super::*; use tokio::sync::oneshot; @@ -194,12 +194,12 @@ impl TransferCompletionHandle for ScheduledTransferCompletionHandle { } async fn mark_complete(&self, result: anyhow::Result<()>) { - if let Some(completion_tx) = self.completion_tx.lock().unwrap().take() { - if completion_tx.send(result).is_err() { - tracing::error!( - "failed to send completion status; this could lead to silent data corruption" - ); - } + if let Some(completion_tx) = self.completion_tx.lock().unwrap().take() + && completion_tx.send(result).is_err() + { + tracing::error!( + "failed to send completion status; this could lead to silent data corruption" + ); } } } @@ -256,8 +256,8 @@ impl TransferCompletionHandle for ImmediateTransferCompletionHandle { let mut guard = self.completion_tx.lock().unwrap(); guard.take() }; - if let Some(completion_tx) = completion_tx { - if completion_tx + if let Some(completion_tx) = completion_tx + && completion_tx .send(TransferToSchedulerMessage::ImmediateResult( ImmediateTransferResult { request_id: self.request_id.clone(), @@ -267,9 +267,8 @@ impl TransferCompletionHandle for ImmediateTransferCompletionHandle { )) .await .is_err() - { - tracing::error!(DISCONNECTED_WARNING); - } + { + tracing::error!(DISCONNECTED_WARNING); } } } diff --git a/lib/llm/src/block_manager/controller.rs b/lib/llm/src/block_manager/controller.rs index 59f70c2646..86e955628c 100644 --- a/lib/llm/src/block_manager/controller.rs +++ b/lib/llm/src/block_manager/controller.rs @@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize}; use dynamo_runtime::{ pipeline::{ - async_trait, network::Ingress, AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, - ResponseStream, SingleIn, + AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, ResponseStream, SingleIn, + async_trait, network::Ingress, }, protocols::annotated::Annotated, traits::DistributedRuntimeProvider, diff --git a/lib/llm/src/block_manager/distributed.rs b/lib/llm/src/block_manager/distributed.rs index 925507b42a..ee338166eb 100644 --- a/lib/llm/src/block_manager/distributed.rs +++ b/lib/llm/src/block_manager/distributed.rs @@ -43,22 +43,22 @@ pub struct SchedulerRequest { mod tests { use super::*; - use crate::block_manager::block::data::logical::distributed_leader_worker::DistributedLeaderWorkerResources; + use crate::block_manager::KvBlockManager; use crate::block_manager::block::BasicMetadata; + use crate::block_manager::block::data::logical::distributed_leader_worker::DistributedLeaderWorkerResources; use crate::block_manager::config::*; use crate::block_manager::locality::Logical; use crate::block_manager::storage::{ - torch::{TorchDevice, TorchTensor}, DeviceAllocator, Storage, StorageAllocator, + torch::{TorchDevice, TorchTensor}, }; - use crate::block_manager::KvBlockManager; use anyhow::Result; use rstest::*; use std::sync::{ - atomic::{AtomicUsize, Ordering}, Arc, + atomic::{AtomicUsize, Ordering}, }; use tokio_util::sync::CancellationToken; diff --git a/lib/llm/src/block_manager/distributed/transfer.rs b/lib/llm/src/block_manager/distributed/transfer.rs index 6629a2fa87..611046853e 100644 --- a/lib/llm/src/block_manager/distributed/transfer.rs +++ b/lib/llm/src/block_manager/distributed/transfer.rs @@ -10,15 +10,15 @@ use zmq::*; use BlockTransferPool::*; use crate::block_manager::{ + BasicMetadata, Storage, block::{ + Block, BlockDataProvider, BlockDataProviderMut, ReadableBlock, WritableBlock, data::local::LocalBlockData, locality, transfer::{TransferContext, WriteTo, WriteToStrategy}, - Block, BlockDataProvider, BlockDataProviderMut, ReadableBlock, WritableBlock, }, connector::scheduler::{SchedulingDecision, TransferSchedulerClient}, storage::{DeviceStorage, DiskStorage, Local, PinnedStorage}, - BasicMetadata, Storage, }; use anyhow::Result; @@ -113,15 +113,13 @@ impl BlockTransferHandler { .collect(); // Perform the transfer, and return the notifying channel. - let channel = match sources.write_to(&mut targets, self.context.clone()) { + match sources.write_to(&mut targets, self.context.clone()) { Ok(channel) => Ok(channel), Err(e) => { tracing::error!("Failed to write to blocks: {:?}", e); Err(e.into()) } - }; - - channel + } } pub async fn execute_transfer(&self, request: BlockTransferRequest) -> Result<()> { diff --git a/lib/llm/src/block_manager/distributed/worker.rs b/lib/llm/src/block_manager/distributed/worker.rs index fcbc7c83de..fc4c9a8232 100644 --- a/lib/llm/src/block_manager/distributed/worker.rs +++ b/lib/llm/src/block_manager/distributed/worker.rs @@ -10,11 +10,11 @@ use utils::*; use zmq::*; use crate::block_manager::{ - block::{layout_to_blocks, locality, transfer::TransferContext, Block}, + BasicMetadata, BlockMetadata, LayoutConfigBuilder, NixlLayout, Storage, + block::{Block, layout_to_blocks, locality, transfer::TransferContext}, connector::scheduler::TransferSchedulerClient, layout::LayoutType, - storage::{torch::TorchTensor, DeviceAllocator, DeviceStorage, DiskAllocator, PinnedAllocator}, - BasicMetadata, BlockMetadata, LayoutConfigBuilder, NixlLayout, Storage, + storage::{DeviceAllocator, DeviceStorage, DiskAllocator, PinnedAllocator, torch::TorchTensor}, }; use derive_builder::Builder; @@ -28,8 +28,8 @@ use tokio::sync::oneshot; use tokio_util::sync::CancellationToken; use dynamo_runtime::{ - utils::{leader_worker_barrier::WorkerBarrier, task::CriticalTaskExecutionHandle}, DistributedRuntime, + utils::{leader_worker_barrier::WorkerBarrier, task::CriticalTaskExecutionHandle}, }; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/lib/llm/src/block_manager/distributed/zmq.rs b/lib/llm/src/block_manager/distributed/zmq.rs index ed8214f77a..a19e0f9585 100644 --- a/lib/llm/src/block_manager/distributed/zmq.rs +++ b/lib/llm/src/block_manager/distributed/zmq.rs @@ -13,13 +13,13 @@ use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::time::{Duration, Instant}; use tmq::{ - publish::{publish, Publish}, - pull::{pull, Pull}, - push::{push, Push}, - subscribe::{subscribe, Subscribe}, Context, Message, Multipart, + publish::{Publish, publish}, + pull::{Pull, pull}, + push::{Push, push}, + subscribe::{Subscribe, subscribe}, }; -use tokio::sync::{oneshot, Mutex}; +use tokio::sync::{Mutex, oneshot}; use tokio_util::sync::CancellationToken; use futures_util::{SinkExt, StreamExt}; diff --git a/lib/llm/src/block_manager/layout/nixl.rs b/lib/llm/src/block_manager/layout/nixl.rs index 3935deaad4..75f0d71ec2 100644 --- a/lib/llm/src/block_manager/layout/nixl.rs +++ b/lib/llm/src/block_manager/layout/nixl.rs @@ -110,8 +110,8 @@ use super::{ }; use super::super::storage::{ - nixl::{NixlAgent, NixlRegisterableStorage, NixlStorage, OptArgs}, Storage, StorageAllocator, + nixl::{NixlAgent, NixlRegisterableStorage, NixlStorage, OptArgs}, }; use super::{FullyContiguous, FullyContiguousConfig, LayerSeparate, LayerSeparateConfig}; use serde::{Deserialize, Serialize}; diff --git a/lib/llm/src/block_manager/metrics.rs b/lib/llm/src/block_manager/metrics.rs index e8b78b6729..1b8174e0cf 100644 --- a/lib/llm/src/block_manager/metrics.rs +++ b/lib/llm/src/block_manager/metrics.rs @@ -15,9 +15,9 @@ use anyhow::Result; use prometheus::{ + IntCounterVec, IntGaugeVec, Opts, Registry, core::{AtomicI64, AtomicU64, GenericCounter, GenericGauge}, - register_int_counter_vec_with_registry, register_int_gauge_vec_with_registry, IntCounterVec, - IntGaugeVec, Opts, Registry, + register_int_counter_vec_with_registry, register_int_gauge_vec_with_registry, }; use std::sync::Arc; pub struct BlockManagerMetrics { diff --git a/lib/llm/src/block_manager/offload.rs b/lib/llm/src/block_manager/offload.rs index 3d5e9f545c..5e0b57d9b7 100644 --- a/lib/llm/src/block_manager/offload.rs +++ b/lib/llm/src/block_manager/offload.rs @@ -45,8 +45,8 @@ //! of the [`OffloadManager::offload_worker`] and [`OffloadManager::onboard_worker`] methods. use super::block::{ - locality::LocalityProvider, transfer::TransferContext, BlockError, BlockMetadata, BlockState, - ImmutableBlock, MutableBlock, + BlockError, BlockMetadata, BlockState, ImmutableBlock, MutableBlock, + locality::LocalityProvider, transfer::TransferContext, }; use super::metrics::{BlockManagerMetrics, PoolMetrics}; use super::pool::{BlockPool, BlockPoolError}; @@ -56,8 +56,9 @@ use nixl_sys::Agent as NixlAgent; use std::sync::Arc; use tokio::runtime::Handle; use tokio::sync::{ + Mutex, mpsc::{self, error::TryRecvError}, - oneshot, Mutex, + oneshot, }; use tokio_util::sync::CancellationToken; @@ -320,20 +321,21 @@ impl if let Ok(blocks) = target_pool .match_sequence_hashes(vec![request.sequence_hash].as_slice()) .await + && !blocks.is_empty() { - if !blocks.is_empty() { - continue; - } + continue; } let target_block = 'target_block: { - if let Ok(blocks) = target_pool.allocate_blocks(1).await { - if let Some(block) = blocks.into_iter().next() { - break 'target_block Some(block); - } + if let Ok(blocks) = target_pool.allocate_blocks(1).await + && let Some(block) = blocks.into_iter().next() + { + break 'target_block Some(block); } - tracing::warn!("Target pool full. Skipping offload. This should only ever happen with very small pool sizes."); + tracing::warn!( + "Target pool full. Skipping offload. This should only ever happen with very small pool sizes." + ); None }; @@ -504,14 +506,14 @@ impl } } - if let Some(targets) = targets.as_ref() { - if targets.len() != blocks.len() { - tx.send(Err(BlockPoolError::BlockError(BlockError::Other( - anyhow::anyhow!("Number of targets does not match number of blocks."), - )))) - .unwrap(); - return rx; - } + if let Some(targets) = targets.as_ref() + && targets.len() != blocks.len() + { + tx.send(Err(BlockPoolError::BlockError(BlockError::Other( + anyhow::anyhow!("Number of targets does not match number of blocks."), + )))) + .unwrap(); + return rx; } if blocks.is_empty() { @@ -582,16 +584,16 @@ mod tests { use super::*; use crate::block_manager::{ + LayoutConfig, NixlRegisterableStorage, block::{ - locality::Local, BasicMetadata, BlockDataExt, BlockDataProvider, Blocks, MutableBlock, + BasicMetadata, BlockDataExt, BlockDataProvider, Blocks, MutableBlock, locality::Local, }, - layout::{nixl::NixlLayout, FullyContiguous, LayerSeparate, LayoutType}, + layout::{FullyContiguous, LayerSeparate, LayoutType, nixl::NixlLayout}, pool::{BlockRegistrationDuplicationSetting, ManagedBlockPool}, storage::{ DeviceAllocator, DeviceStorage, DiskAllocator, DiskStorage, PinnedAllocator, PinnedStorage, StorageAllocator, StorageType, }, - LayoutConfig, NixlRegisterableStorage, }; use crate::tokens::{TokenBlockSequence, Tokens}; use nixl_sys::{MemoryRegion, NixlDescriptor}; diff --git a/lib/llm/src/block_manager/offload/pending.rs b/lib/llm/src/block_manager/offload/pending.rs index 585cf28ace..7aee34c989 100644 --- a/lib/llm/src/block_manager/offload/pending.rs +++ b/lib/llm/src/block_manager/offload/pending.rs @@ -48,10 +48,10 @@ use tokio::sync::{mpsc, oneshot}; use tokio_util::sync::CancellationToken; use crate::block_manager::block::{ - locality::LocalityProvider, - transfer::{TransferContext, WriteTo, WriteToStrategy}, BlockDataProvider, BlockDataProviderMut, BlockError, BlockMetadata, BlockState, ImmutableBlock, MutableBlock, ReadableBlock, WritableBlock, + locality::LocalityProvider, + transfer::{TransferContext, WriteTo, WriteToStrategy}, }; use crate::block_manager::metrics::PoolMetrics; use crate::block_manager::pool::{BlockPool, BlockPoolError}; @@ -59,7 +59,7 @@ use crate::block_manager::storage::{Local, Storage}; use anyhow::Result; use async_trait::async_trait; -use futures::{stream::FuturesUnordered, StreamExt}; +use futures::{StreamExt, stream::FuturesUnordered}; use super::BlockResult; diff --git a/lib/llm/src/block_manager/offload/request.rs b/lib/llm/src/block_manager/offload/request.rs index c73ed7e8da..500c309552 100644 --- a/lib/llm/src/block_manager/offload/request.rs +++ b/lib/llm/src/block_manager/offload/request.rs @@ -18,7 +18,7 @@ use std::sync::Weak; use tokio::sync::oneshot; use crate::block_manager::block::{ - locality::LocalityProvider, BlockMetadata, ImmutableBlock, MutableBlock, + BlockMetadata, ImmutableBlock, MutableBlock, locality::LocalityProvider, }; use crate::block_manager::pool::BlockPoolError; use crate::block_manager::storage::Storage; diff --git a/lib/llm/src/block_manager/pool.rs b/lib/llm/src/block_manager/pool.rs index 0650c9757c..6c5ea524de 100644 --- a/lib/llm/src/block_manager/pool.rs +++ b/lib/llm/src/block_manager/pool.rs @@ -23,15 +23,15 @@ use serde::{Deserialize, Serialize}; pub use super::block::{ImmutableBlock, MutableBlock}; use super::block::{ - nixl::short_type_name, private, registry::BlockRegistry, Block, BlockError, BlockMetadata, - GlobalRegistry, MaybeReturnableBlock, + Block, BlockError, BlockMetadata, GlobalRegistry, MaybeReturnableBlock, nixl::short_type_name, + private, registry::BlockRegistry, }; use super::events::{EventManager, NullEventManager}; use super::metrics::{BlockManagerMetrics, PoolMetrics}; use super::storage::Storage; -use crate::block_manager::block::locality::LocalityProvider; use crate::block_manager::CacheLevel; +use crate::block_manager::block::locality::LocalityProvider; use crate::tokens::{SequenceHash, TokenBlock}; use async_trait::async_trait; diff --git a/lib/llm/src/block_manager/pool/managed.rs b/lib/llm/src/block_manager/pool/managed.rs index 17fc986158..ab6dce3856 100644 --- a/lib/llm/src/block_manager/pool/managed.rs +++ b/lib/llm/src/block_manager/pool/managed.rs @@ -589,7 +589,7 @@ impl ProgressEngine #[cfg(test)] mod tests { use crate::block_manager::block::{BasicMetadata, Blocks}; - use crate::block_manager::layout::{tests::setup_layout, FullyContiguous, LayoutConfig}; + use crate::block_manager::layout::{FullyContiguous, LayoutConfig, tests::setup_layout}; use crate::block_manager::locality::Local; use crate::tokens::{TokenBlockSequence, Tokens}; diff --git a/lib/llm/src/block_manager/pool/managed/active.rs b/lib/llm/src/block_manager/pool/managed/active.rs index 8aa292f238..fae30eeb42 100644 --- a/lib/llm/src/block_manager/pool/managed/active.rs +++ b/lib/llm/src/block_manager/pool/managed/active.rs @@ -51,10 +51,10 @@ impl ActiveBlockPool // Set the parent of the block if it has one. // This is needed to ensure the lifetime of the parent is at least as long as the child. - if let Ok(Some(parent)) = block.parent_sequence_hash() { - if let Some(parent_block) = self.match_sequence_hash(parent) { - block.set_parent(parent_block.mutable_block().clone()); - } + if let Ok(Some(parent)) = block.parent_sequence_hash() + && let Some(parent_block) = self.match_sequence_hash(parent) + { + block.set_parent(parent_block.mutable_block().clone()); } let shared = Arc::new(block); @@ -78,14 +78,14 @@ impl ActiveBlockPool } pub fn remove(&mut self, block: &mut Block) { - if let Ok(sequence_hash) = block.sequence_hash() { - if let Some(weak) = self.map.get(&sequence_hash) { - if let Some(_arc) = weak.upgrade() { - block.reset(); - return; - } - self.map.remove(&sequence_hash); + if let Ok(sequence_hash) = block.sequence_hash() + && let Some(weak) = self.map.get(&sequence_hash) + { + if let Some(_arc) = weak.upgrade() { + block.reset(); + return; } + self.map.remove(&sequence_hash); } } diff --git a/lib/llm/src/block_manager/pool/managed/inactive.rs b/lib/llm/src/block_manager/pool/managed/inactive.rs index e287e3960b..4471194d10 100644 --- a/lib/llm/src/block_manager/pool/managed/inactive.rs +++ b/lib/llm/src/block_manager/pool/managed/inactive.rs @@ -15,7 +15,7 @@ use std::sync::atomic::AtomicU64; -use crate::block_manager::block::{locality::LocalityProvider, BlockState}; +use crate::block_manager::block::{BlockState, locality::LocalityProvider}; use super::*; use priority_key::PriorityKey; @@ -113,7 +113,9 @@ impl InactiveBlockPool, sequence_hash: SequenceHash) { let priority_key = PriorityKey::new(block.metadata().clone(), sequence_hash); if self.priority_set.contains(&priority_key) { - tracing::trace!("multiple entries with the same sequence hash, resetting block and inserting into uninitialized set"); + tracing::trace!( + "multiple entries with the same sequence hash, resetting block and inserting into uninitialized set" + ); let mut block = block; block.reset(); self.uninitialized_set.push_back(block); @@ -546,8 +548,8 @@ pub(crate) mod tests { use crate::{ block_manager::{ block::{ - locality::Local, registry::BlockRegistry, state::CompleteState, Blocks, - PrivateBlockExt, + Blocks, PrivateBlockExt, locality::Local, registry::BlockRegistry, + state::CompleteState, }, events::NullEventManager, layout::{BlockLayout, FullyContiguous, LayoutConfigBuilder}, diff --git a/lib/llm/src/block_manager/pool/managed/state.rs b/lib/llm/src/block_manager/pool/managed/state.rs index 53bfaf39ef..998940e9fd 100644 --- a/lib/llm/src/block_manager/pool/managed/state.rs +++ b/lib/llm/src/block_manager/pool/managed/state.rs @@ -14,7 +14,7 @@ // limitations under the License. use crate::block_manager::{ - block::{registry::BlockRegistrationError, BlockState, PrivateBlockExt}, + block::{BlockState, PrivateBlockExt, registry::BlockRegistrationError}, events::Publisher, }; @@ -266,18 +266,16 @@ impl State } } BlockRegistrationDuplicationSetting::Disabled => { - if let Some(block) = duplicate { - if let Some(raw_blocks) = block.try_take_block(private::PrivateToken) { - self.inactive.return_blocks(raw_blocks); - } + if let Some(block) = duplicate + && let Some(raw_blocks) = block.try_take_block(private::PrivateToken) + { + self.inactive.return_blocks(raw_blocks); } } } - if offload { - if let Some(priority) = immutable.metadata().offload_priority() { - immutable.enqueue_offload(priority).await.unwrap(); - } + if offload && let Some(priority) = immutable.metadata().offload_priority() { + immutable.enqueue_offload(priority).await.unwrap(); } immutable_blocks.push(immutable); diff --git a/lib/llm/src/block_manager/state.rs b/lib/llm/src/block_manager/state.rs index 5ea44d6f24..1601dad984 100644 --- a/lib/llm/src/block_manager/state.rs +++ b/lib/llm/src/block_manager/state.rs @@ -17,7 +17,7 @@ mod local; mod logical; mod resources; -use crate::block_manager::block::{factory::IntoBlocks, MutableBlock}; +use crate::block_manager::block::{MutableBlock, factory::IntoBlocks}; use crate::block_manager::locality::LogicalResources; use crate::block_manager::offload::request::BlockResult; @@ -26,8 +26,8 @@ use super::*; // use super::offload::OffloadManager; use super::{ block::{ - factory::LocalBlockDataFactory, locality::LocalityProvider, Block, GlobalRegistry, - ImmutableBlock, + Block, GlobalRegistry, ImmutableBlock, factory::LocalBlockDataFactory, + locality::LocalityProvider, }, config::NixlOptions, events::{EventManager, NullEventManager}, diff --git a/lib/llm/src/block_manager/storage.rs b/lib/llm/src/block_manager/storage.rs index ba23466f4e..597c3d052e 100644 --- a/lib/llm/src/block_manager/storage.rs +++ b/lib/llm/src/block_manager/storage.rs @@ -88,7 +88,7 @@ pub use disk::*; use torch::*; use std::{ - alloc::{alloc_zeroed, dealloc, Layout}, + alloc::{Layout, alloc_zeroed, dealloc}, collections::HashMap, fmt::Debug, ptr::NonNull, @@ -322,7 +322,10 @@ impl std::fmt::Debug for RegistrationHandles { impl Drop for RegistrationHandles { fn drop(&mut self) { if !self.handles.is_empty() { - panic!("RegistrationHandles dropped with {} handles remaining; RegistrationHandles::release() needs to be explicitly called", self.handles.len()); + panic!( + "RegistrationHandles dropped with {} handles remaining; RegistrationHandles::release() needs to be explicitly called", + self.handles.len() + ); } } } diff --git a/lib/llm/src/block_manager/storage/arena.rs b/lib/llm/src/block_manager/storage/arena.rs index fc503c2ddd..519320a038 100644 --- a/lib/llm/src/block_manager/storage/arena.rs +++ b/lib/llm/src/block_manager/storage/arena.rs @@ -207,7 +207,7 @@ mod nixl { S: MemoryRegion, { unsafe fn as_ptr(&self) -> *const u8 { - Storage::as_ptr(self.storage.as_ref()) + unsafe { Storage::as_ptr(self.storage.as_ref()) } } fn size(&self) -> usize { diff --git a/lib/llm/src/block_manager/storage/cuda.rs b/lib/llm/src/block_manager/storage/cuda.rs index 3263bf9c51..f4d25c8027 100644 --- a/lib/llm/src/block_manager/storage/cuda.rs +++ b/lib/llm/src/block_manager/storage/cuda.rs @@ -86,7 +86,7 @@ use std::{ sync::{Arc, Mutex, OnceLock}, }; -use cudarc::driver::{sys, CudaContext}; +use cudarc::driver::{CudaContext, sys}; /// Trait for [Storage] types that can be accessed by CUDA pub trait CudaAccessible: Storage {} diff --git a/lib/llm/src/block_manager/storage/disk.rs b/lib/llm/src/block_manager/storage/disk.rs index 3d7490ce80..2ce7cff228 100644 --- a/lib/llm/src/block_manager/storage/disk.rs +++ b/lib/llm/src/block_manager/storage/disk.rs @@ -16,7 +16,7 @@ use super::*; use core::ffi::c_char; -use nix::fcntl::{fallocate, FallocateFlags}; +use nix::fcntl::{FallocateFlags, fallocate}; use nix::unistd::unlink; use std::ffi::CStr; use std::ffi::CString; diff --git a/lib/llm/src/block_manager/storage/nixl.rs b/lib/llm/src/block_manager/storage/nixl.rs index 50e0d74711..95b0159805 100644 --- a/lib/llm/src/block_manager/storage/nixl.rs +++ b/lib/llm/src/block_manager/storage/nixl.rs @@ -342,7 +342,7 @@ impl NixlRegisterableStorage for PinnedStorage {} impl MemoryRegion for PinnedStorage { unsafe fn as_ptr(&self) -> *const u8 { - Storage::as_ptr(self) + unsafe { Storage::as_ptr(self) } } fn size(&self) -> usize { @@ -367,7 +367,7 @@ impl NixlRegisterableStorage for DeviceStorage {} impl MemoryRegion for DeviceStorage { unsafe fn as_ptr(&self) -> *const u8 { - Storage::as_ptr(self) + unsafe { Storage::as_ptr(self) } } fn size(&self) -> usize { @@ -406,7 +406,7 @@ impl NixlRegisterableStorage for DiskStorage { impl MemoryRegion for DiskStorage { unsafe fn as_ptr(&self) -> *const u8 { - Storage::as_ptr(self) + unsafe { Storage::as_ptr(self) } } fn size(&self) -> usize { diff --git a/lib/llm/src/cuda.rs b/lib/llm/src/cuda.rs index 92258a5ad9..37e0965920 100644 --- a/lib/llm/src/cuda.rs +++ b/lib/llm/src/cuda.rs @@ -17,8 +17,8 @@ //! them within Dynamo. use cudarc::driver::{ - sys::{cuCtxPopCurrent_v2, cuCtxPushCurrent_v2, cudaError_enum, CUcontext, CUstream}, CudaContext, CudaStream, + sys::{CUcontext, CUstream, cuCtxPopCurrent_v2, cuCtxPushCurrent_v2, cudaError_enum}, }; use std::pin::Pin; use std::{marker::PhantomData, sync::Arc}; diff --git a/lib/llm/src/disagg_router.rs b/lib/llm/src/disagg_router.rs index 17339562f5..94a8469b3d 100644 --- a/lib/llm/src/disagg_router.rs +++ b/lib/llm/src/disagg_router.rs @@ -18,8 +18,8 @@ use std::sync::{Arc, Mutex}; use tokio::sync::watch; use tracing; -use dynamo_runtime::transports::etcd::WatchEvent; use dynamo_runtime::DistributedRuntime; +use dynamo_runtime::transports::etcd::WatchEvent; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DisaggRouterConf { @@ -218,23 +218,23 @@ impl DisaggregatedRouter { } pub fn check_for_updates(&self) { - if let Some(watcher) = &self.config_watcher { - if watcher.has_changed().unwrap_or(false) { - let config = watcher.borrow().clone(); - let new_value = config.max_local_prefill_length; - - // Update the value using the mutex - let mut current_value = self.max_local_prefill_length.lock().unwrap(); - let old_value = *current_value; - if old_value != new_value { - *current_value = new_value; - tracing::info!( - "Applied config update for model {}: max_local_prefill_length changed from {} to {}", - self.model_name, - old_value, - new_value - ); - } + if let Some(watcher) = &self.config_watcher + && watcher.has_changed().unwrap_or(false) + { + let config = watcher.borrow().clone(); + let new_value = config.max_local_prefill_length; + + // Update the value using the mutex + let mut current_value = self.max_local_prefill_length.lock().unwrap(); + let old_value = *current_value; + if old_value != new_value { + *current_value = new_value; + tracing::info!( + "Applied config update for model {}: max_local_prefill_length changed from {} to {}", + self.model_name, + old_value, + new_value + ); } } } diff --git a/lib/llm/src/discovery/model_manager.rs b/lib/llm/src/discovery/model_manager.rs index 95e05baca0..41523d6bfe 100644 --- a/lib/llm/src/discovery/model_manager.rs +++ b/lib/llm/src/discovery/model_manager.rs @@ -7,7 +7,7 @@ use dynamo_runtime::slug::Slug; use crate::discovery::ModelEntry; -use crate::kv_router::{scheduler::DefaultWorkerSelector, KvRouterConfig}; +use crate::kv_router::{KvRouterConfig, scheduler::DefaultWorkerSelector}; use crate::{ kv_router::KvRouter, types::openai::{ diff --git a/lib/llm/src/discovery/watcher.rs b/lib/llm/src/discovery/watcher.rs index 1f27a9ed36..219fd95804 100644 --- a/lib/llm/src/discovery/watcher.rs +++ b/lib/llm/src/discovery/watcher.rs @@ -5,16 +5,16 @@ use std::sync::Arc; use tokio::sync::mpsc::Sender; use anyhow::Context as _; -use tokio::sync::{mpsc::Receiver, Notify}; +use tokio::sync::{Notify, mpsc::Receiver}; use dynamo_runtime::{ + DistributedRuntime, pipeline::{ - network::egress::push_router::PushRouter, ManyOut, Operator, RouterMode, SegmentSource, - ServiceBackend, SingleIn, Source, + ManyOut, Operator, RouterMode, SegmentSource, ServiceBackend, SingleIn, Source, + network::egress::push_router::PushRouter, }, protocols::annotated::Annotated, transports::etcd::{KeyValue, WatchEvent}, - DistributedRuntime, }; use crate::{ @@ -35,7 +35,7 @@ use crate::{ }, }; -use super::{ModelEntry, ModelManager, MODEL_ROOT_PATH}; +use super::{MODEL_ROOT_PATH, ModelEntry, ModelManager}; #[derive(Debug, Clone, Copy, PartialEq)] pub enum ModelUpdate { @@ -213,10 +213,8 @@ impl ModelWatcher { ); update_tx = false; } - if update_tx { - if let Some(tx) = &self.model_update_tx { - tx.send(ModelUpdate::Removed(model_type)).await.ok(); - } + if update_tx && let Some(tx) = &self.model_update_tx { + tx.send(ModelUpdate::Removed(model_type)).await.ok(); } return Ok(None); } @@ -251,13 +249,12 @@ impl ModelWatcher { ); } else { for model_type in ALL_MODEL_TYPES { - if (chat_model_removed && *model_type == ModelType::Chat) + if ((chat_model_removed && *model_type == ModelType::Chat) || (completions_model_removed && *model_type == ModelType::Completion) - || (embeddings_model_removed && *model_type == ModelType::Embedding) + || (embeddings_model_removed && *model_type == ModelType::Embedding)) + && let Some(tx) = &self.model_update_tx { - if let Some(tx) = &self.model_update_tx { - tx.send(ModelUpdate::Removed(*model_type)).await.ok(); - } + tx.send(ModelUpdate::Removed(*model_type)).await.ok(); } } } diff --git a/lib/llm/src/engines.rs b/lib/llm/src/engines.rs index bd7e0e6e3c..59c5e15c21 100644 --- a/lib/llm/src/engines.rs +++ b/lib/llm/src/engines.rs @@ -18,7 +18,7 @@ use crate::preprocessor::PreprocessedRequest; use crate::protocols::common::llm_backend::LLMEngineOutput; use crate::protocols::openai::{ chat_completions::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse}, - completions::{prompt_to_string, NvCreateCompletionRequest, NvCreateCompletionResponse}, + completions::{NvCreateCompletionRequest, NvCreateCompletionResponse, prompt_to_string}, }; use crate::types::openai::embeddings::NvCreateEmbeddingRequest; use crate::types::openai::embeddings::NvCreateEmbeddingResponse; diff --git a/lib/llm/src/entrypoint/input/batch.rs b/lib/llm/src/entrypoint/input/batch.rs index 3943a4792f..bb18a01d9e 100644 --- a/lib/llm/src/entrypoint/input/batch.rs +++ b/lib/llm/src/entrypoint/input/batch.rs @@ -8,18 +8,18 @@ use crate::types::openai::chat_completions::{ }; use anyhow::Context as _; use dynamo_async_openai::types::FinishReason; -use dynamo_runtime::{pipeline::Context, runtime::CancellationToken, Runtime}; +use dynamo_runtime::{Runtime, pipeline::Context, runtime::CancellationToken}; use futures::StreamExt; use serde::{Deserialize, Serialize}; use std::cmp; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; -use crate::entrypoint::input::common; use crate::entrypoint::EngineConfig; +use crate::entrypoint::input::common; /// Max tokens in each response. /// TODO: For batch mode this should be the full context size of the model diff --git a/lib/llm/src/entrypoint/input/common.rs b/lib/llm/src/entrypoint/input/common.rs index 16aa6ad5ee..5aa20f3f4e 100644 --- a/lib/llm/src/entrypoint/input/common.rs +++ b/lib/llm/src/entrypoint/input/common.rs @@ -5,7 +5,7 @@ use std::pin::Pin; use crate::{ backend::{Backend, ExecutionContext}, - discovery::{ModelManager, ModelWatcher, MODEL_ROOT_PATH}, + discovery::{MODEL_ROOT_PATH, ModelManager, ModelWatcher}, engines::StreamingEngineAdapter, entrypoint::{self, EngineConfig}, kv_router::{KvPushRouter, KvRouter}, @@ -15,15 +15,16 @@ use crate::{ protocols::common::llm_backend::{BackendOutput, LLMEngineOutput, PreprocessedRequest}, request_template::RequestTemplate, types::{ + Annotated, openai::chat_completions::{ NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse, OpenAIChatCompletionsStreamingEngine, }, - Annotated, }, }; use dynamo_runtime::{ + DistributedRuntime, Runtime, component::Client, distributed::DistributedConfig, engine::{AsyncEngineStream, Data}, @@ -31,7 +32,6 @@ use dynamo_runtime::{ Context, ManyOut, Operator, PushRouter, RouterMode, SegmentSource, ServiceBackend, ServiceEngine, ServiceFrontend, SingleIn, Source, }, - DistributedRuntime, Runtime, }; use std::sync::Arc; @@ -191,11 +191,11 @@ where Req: Data, Resp: Data, OpenAIPreprocessor: Operator< - Context, - Pin>>>, - Context, - Pin>>>, - >, + Context, + Pin>>>, + Context, + Pin>>>, + >, { let frontend = ServiceFrontend::, ManyOut>>::new(); let preprocessor = OpenAIPreprocessor::new((*card).clone()) @@ -224,11 +224,11 @@ where Req: Data, Resp: Data, OpenAIPreprocessor: Operator< - Context, - Pin>>>, - Context, - Pin>>>, - >, + Context, + Pin>>>, + Context, + Pin>>>, + >, { let frontend = SegmentSource::, ManyOut>>::new(); let preprocessor = OpenAIPreprocessor::new(card.clone()).await?.into_operator(); diff --git a/lib/llm/src/entrypoint/input/endpoint.rs b/lib/llm/src/entrypoint/input/endpoint.rs index ff10ffa2b3..c6c36a0a83 100644 --- a/lib/llm/src/entrypoint/input/endpoint.rs +++ b/lib/llm/src/entrypoint/input/endpoint.rs @@ -9,18 +9,18 @@ use crate::{ model_type::ModelType, preprocessor::{BackendOutput, PreprocessedRequest}, types::{ + Annotated, openai::chat_completions::{ NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse, }, - Annotated, }, }; use dynamo_runtime::engine::AsyncEngineStream; use dynamo_runtime::pipeline::{ - network::Ingress, Context, ManyOut, Operator, SegmentSource, ServiceBackend, SingleIn, Source, + Context, ManyOut, Operator, SegmentSource, ServiceBackend, SingleIn, Source, network::Ingress, }; -use dynamo_runtime::{protocols::EndpointId, DistributedRuntime}; +use dynamo_runtime::{DistributedRuntime, protocols::EndpointId}; use crate::entrypoint::EngineConfig; @@ -125,13 +125,12 @@ pub async fn run( result?; // Cleanup on shutdown - if let Some(mut card) = card { - if let Err(err) = card + if let Some(mut card) = card + && let Err(err) = card .delete_from_nats(distributed_runtime.nats_client()) .await - { - tracing::error!(%err, "delete_from_nats error on shutdown"); - } + { + tracing::error!(%err, "delete_from_nats error on shutdown"); } Ok(()) diff --git a/lib/llm/src/entrypoint/input/http.rs b/lib/llm/src/entrypoint/input/http.rs index 0437ee902a..a8e715309c 100644 --- a/lib/llm/src/entrypoint/input/http.rs +++ b/lib/llm/src/entrypoint/input/http.rs @@ -4,10 +4,10 @@ use std::sync::Arc; use crate::{ - discovery::{ModelManager, ModelUpdate, ModelWatcher, MODEL_ROOT_PATH}, + discovery::{MODEL_ROOT_PATH, ModelManager, ModelUpdate, ModelWatcher}, endpoint_type::EndpointType, engines::StreamingEngineAdapter, - entrypoint::{self, input::common, EngineConfig}, + entrypoint::{self, EngineConfig, input::common}, http::service::service_v2::{self, HttpService}, kv_router::KvRouterConfig, model_type::ModelType, @@ -17,8 +17,8 @@ use crate::{ }, }; use dynamo_runtime::transports::etcd; -use dynamo_runtime::{distributed::DistributedConfig, pipeline::RouterMode}; use dynamo_runtime::{DistributedRuntime, Runtime}; +use dynamo_runtime::{distributed::DistributedConfig, pipeline::RouterMode}; /// Build and run an HTTP service pub async fn run(runtime: Runtime, engine_config: EngineConfig) -> anyhow::Result<()> { diff --git a/lib/llm/src/entrypoint/input/text.rs b/lib/llm/src/entrypoint/input/text.rs index 68c3c17d65..c29486d70c 100644 --- a/lib/llm/src/entrypoint/input/text.rs +++ b/lib/llm/src/entrypoint/input/text.rs @@ -6,12 +6,12 @@ use crate::request_template::RequestTemplate; use crate::types::openai::chat_completions::{ NvCreateChatCompletionRequest, OpenAIChatCompletionsStreamingEngine, }; -use dynamo_runtime::{pipeline::Context, runtime::CancellationToken, Runtime}; +use dynamo_runtime::{Runtime, pipeline::Context, runtime::CancellationToken}; use futures::StreamExt; use std::io::{ErrorKind, Write}; -use crate::entrypoint::input::common; use crate::entrypoint::EngineConfig; +use crate::entrypoint::input::common; /// Max response tokens for each single query. Must be less than model context size. /// TODO: Cmd line flag to overwrite this diff --git a/lib/llm/src/gguf/content.rs b/lib/llm/src/gguf/content.rs index 4b825681c0..298b8cda61 100644 --- a/lib/llm/src/gguf/content.rs +++ b/lib/llm/src/gguf/content.rs @@ -29,8 +29,8 @@ use std::collections::HashMap; use anyhow::Context; use candle_core::{ - quantized::gguf_file::{self, Value}, Result, + quantized::gguf_file::{self, Value}, }; use tracing::info; @@ -66,7 +66,9 @@ impl Content { accum }); if n_splits.len() > 1 { - candle_core::bail!("GGUF files have differing `split.count` values: {n_splits:?}. Perhaps the GGUF files do not match?"); + candle_core::bail!( + "GGUF files have differing `split.count` values: {n_splits:?}. Perhaps the GGUF files do not match?" + ); } #[allow(clippy::cast_possible_truncation)] if !n_splits.is_empty() && n_readers != n_splits[0] as usize { diff --git a/lib/llm/src/gguf/gguf_metadata.rs b/lib/llm/src/gguf/gguf_metadata.rs index 2e5059ae2f..0fd786d074 100644 --- a/lib/llm/src/gguf/gguf_metadata.rs +++ b/lib/llm/src/gguf/gguf_metadata.rs @@ -26,8 +26,8 @@ // SOFTWARE. use akin::akin; -use anyhow::ensure; use anyhow::Result; +use anyhow::ensure; use candle_core::quantized::gguf_file; use std::collections::HashMap; use tracing::warn; diff --git a/lib/llm/src/gguf/gguf_tokenizer.rs b/lib/llm/src/gguf/gguf_tokenizer.rs index 65ba55b0b7..03b7a58efc 100644 --- a/lib/llm/src/gguf/gguf_tokenizer.rs +++ b/lib/llm/src/gguf/gguf_tokenizer.rs @@ -31,6 +31,7 @@ use ahash::AHashMap; use anyhow::Result; use itertools::Itertools; use tokenizers::{ + AddedToken, DecoderWrapper, ModelWrapper, NormalizerWrapper, Tokenizer, decoders::{ self, byte_fallback::ByteFallback, byte_level::ByteLevel, fuse::Fuse, strip::Strip, }, @@ -41,7 +42,6 @@ use tokenizers::{ self, template::{self, TemplateProcessing}, }, - AddedToken, DecoderWrapper, ModelWrapper, NormalizerWrapper, Tokenizer, }; use tracing::info; @@ -402,7 +402,7 @@ impl TryFrom> for NormalizerWrapper { #[cfg(test)] mod tests { use anyhow::Result; - use hf_hub::{api::sync::ApiBuilder, Repo, RepoType}; + use hf_hub::{Repo, RepoType, api::sync::ApiBuilder}; use tokenizers::Tokenizer; #[allow(dead_code)] diff --git a/lib/llm/src/http/client.rs b/lib/llm/src/http/client.rs index 2e88cf754c..4809979856 100644 --- a/lib/llm/src/http/client.rs +++ b/lib/llm/src/http/client.rs @@ -14,7 +14,7 @@ use std::time::Instant; use async_trait::async_trait; use derive_getters::Dissolve; -use dynamo_async_openai::{config::OpenAIConfig, error::OpenAIError, Client}; +use dynamo_async_openai::{Client, config::OpenAIConfig, error::OpenAIError}; use futures::Stream; use serde_json::Value; use tokio_util::sync::CancellationToken; @@ -22,10 +22,10 @@ use tracing; use uuid::Uuid; // Import our existing recording infrastructure +use crate::protocols::Annotated; use crate::protocols::openai::chat_completions::{ NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse, }; -use crate::protocols::Annotated; use dynamo_runtime::engine::{ AsyncEngineContext, AsyncEngineContextProvider, AsyncEngineStream, Data, DataStream, }; @@ -523,7 +523,7 @@ impl GenericBYOTClient { #[cfg(test)] mod tests { use super::*; - use tokio::time::{sleep, Duration}; + use tokio::time::{Duration, sleep}; #[tokio::test] async fn test_http_request_context_creation() { diff --git a/lib/llm/src/http/service/health.rs b/lib/llm/src/http/service/health.rs index 487868f3b0..c5825d0f87 100644 --- a/lib/llm/src/http/service/health.rs +++ b/lib/llm/src/http/service/health.rs @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use super::{service_v2, RouteDoc}; -use axum::{http::Method, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use super::{RouteDoc, service_v2}; +use axum::{Json, Router, http::Method, http::StatusCode, response::IntoResponse, routing::get}; use dynamo_runtime::instances::list_all_instances; use serde_json::json; use std::sync::Arc; diff --git a/lib/llm/src/http/service/metrics.rs b/lib/llm/src/http/service/metrics.rs index 2b21e789f1..bb9044eb2c 100644 --- a/lib/llm/src/http/service/metrics.rs +++ b/lib/llm/src/http/service/metrics.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Router}; +use axum::{Router, extract::State, http::StatusCode, response::IntoResponse, routing::get}; use prometheus::{Encoder, HistogramOpts, HistogramVec, IntCounterVec, IntGaugeVec, Opts}; use std::{ sync::Arc, @@ -538,7 +538,7 @@ async fn handler_metrics(State(registry): State>) -> impl IntoResp StatusCode::INTERNAL_SERVER_ERROR, "Failed to encode metrics", ) - .into_response() + .into_response(); } }; diff --git a/lib/llm/src/http/service/openai.rs b/lib/llm/src/http/service/openai.rs index 8a2c9c53a0..d81877f6b7 100644 --- a/lib/llm/src/http/service/openai.rs +++ b/lib/llm/src/http/service/openai.rs @@ -8,36 +8,37 @@ use std::{ }; use axum::{ + Json, Router, extract::State, http::{HeaderMap, StatusCode}, response::{ - sse::{Event, KeepAlive, Sse}, IntoResponse, Response, + sse::{Event, KeepAlive, Sse}, }, routing::{get, post}, - Json, Router, }; use dynamo_runtime::{ pipeline::{AsyncEngineContextProvider, Context}, protocols::annotated::AnnotationsProvider, }; -use futures::{stream, StreamExt}; +use futures::{StreamExt, stream}; use serde::{Deserialize, Serialize}; use super::{ - disconnect::{create_connection_monitor, monitor_for_disconnects, ConnectionHandle}, + RouteDoc, + disconnect::{ConnectionHandle, create_connection_monitor, monitor_for_disconnects}, error::HttpError, metrics::{Endpoint, ResponseMetricCollector}, - service_v2, RouteDoc, + service_v2, }; use crate::preprocessor::LLMMetricAnnotation; use crate::protocols::openai::chat_completions::aggregator::ChatCompletionAggregator; use crate::protocols::openai::{ + ParsingOptions, chat_completions::{NvCreateChatCompletionRequest, NvCreateChatCompletionResponse}, completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, embeddings::{NvCreateEmbeddingRequest, NvCreateEmbeddingResponse}, responses::{NvCreateResponse, NvResponse}, - ParsingOptions, }; use crate::request_template::RequestTemplate; use crate::types::Annotated; @@ -124,18 +125,17 @@ impl ErrorMessage { // First check for PipelineError::ServiceOverloaded if let Some(pipeline_err) = err.downcast_ref::() - { - if matches!( + && matches!( pipeline_err, dynamo_runtime::pipeline::error::PipelineError::ServiceOverloaded(_) - ) { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ErrorMessage { - error: pipeline_err.to_string(), - }), - ); - } + ) + { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ErrorMessage { + error: pipeline_err.to_string(), + }), + ); } // Then check for HttpError @@ -166,17 +166,17 @@ impl From for ErrorMessage { /// Get the request ID from a primary source, or next from the headers, or lastly create a new one if not present fn get_or_create_request_id(primary: Option<&str>, headers: &HeaderMap) -> String { // Try to get request id from trace context - if let Some(trace_context) = get_distributed_tracing_context() { - if let Some(x_dynamo_request_id) = trace_context.x_dynamo_request_id { - return x_dynamo_request_id; - } + if let Some(trace_context) = get_distributed_tracing_context() + && let Some(x_dynamo_request_id) = trace_context.x_dynamo_request_id + { + return x_dynamo_request_id; } // Try to get the request ID from the primary source - if let Some(primary) = primary { - if let Ok(uuid) = uuid::Uuid::parse_str(primary) { - return uuid.to_string(); - } + if let Some(primary) = primary + && let Ok(uuid) = uuid::Uuid::parse_str(primary) + { + return uuid.to_string(); } // Try to get the request ID header as a string slice @@ -792,7 +792,9 @@ pub fn validate_response_input_is_text_only( ) -> Option { match &request.inner.input { dynamo_async_openai::types::responses::Input::Text(_) => None, - _ => Some(ErrorMessage::not_implemented_error("Only `Input::Text` is supported. Structured, multimedia, or custom input types are not yet implemented.")), + _ => Some(ErrorMessage::not_implemented_error( + "Only `Input::Text` is supported. Structured, multimedia, or custom input types are not yet implemented.", + )), } } diff --git a/lib/llm/src/http/service/service_v2.rs b/lib/llm/src/http/service/service_v2.rs index c217361299..99b8dad095 100644 --- a/lib/llm/src/http/service/service_v2.rs +++ b/lib/llm/src/http/service/service_v2.rs @@ -4,14 +4,14 @@ use std::collections::HashMap; use std::env::var; use std::path::PathBuf; +use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -use std::sync::Arc; use std::time::Duration; -use super::metrics; use super::Metrics; use super::RouteDoc; +use super::metrics; use crate::discovery::ModelManager; use crate::endpoint_type::EndpointType; use crate::request_template::RequestTemplate; diff --git a/lib/llm/src/kv_router.rs b/lib/llm/src/kv_router.rs index 3f12681766..f81ca7b530 100644 --- a/lib/llm/src/kv_router.rs +++ b/lib/llm/src/kv_router.rs @@ -9,8 +9,8 @@ use anyhow::Result; use dynamo_runtime::{ component::{Component, InstanceSource}, pipeline::{ - async_trait, AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, PushRouter, - ResponseStream, SingleIn, + AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, PushRouter, ResponseStream, + SingleIn, async_trait, }, prelude::*, protocols::annotated::Annotated, @@ -30,12 +30,12 @@ pub mod scoring; pub mod sequence; use crate::{ - discovery::{ModelEntry, MODEL_ROOT_PATH}, + discovery::{MODEL_ROOT_PATH, ModelEntry}, kv_router::{ approx::ApproxKvIndexer, indexer::{ - compute_block_hash_for_seq, compute_seq_hash_for_block, KvIndexer, KvIndexerInterface, - KvRouterError, OverlapScores, RouterEvent, + KvIndexer, KvIndexerInterface, KvRouterError, OverlapScores, RouterEvent, + compute_block_hash_for_seq, compute_seq_hash_for_block, }, protocols::{LocalBlockHash, RouterRequest, RouterResponse, WorkerSelectionResult}, scheduler::{KvScheduler, KvSchedulerError, SchedulingRequest}, diff --git a/lib/llm/src/kv_router/approx.rs b/lib/llm/src/kv_router/approx.rs index cf7476cbff..af5266d96c 100644 --- a/lib/llm/src/kv_router/approx.rs +++ b/lib/llm/src/kv_router/approx.rs @@ -25,15 +25,15 @@ use tokio_util::sync::CancellationToken; use crate::tokens::{SequenceHash, TokenBlockSequence}; +use crate::kv_router::RouterEvent; use crate::kv_router::indexer::{ - compute_block_hash_for_seq, DumpRequest, KvIndexerInterface, KvRouterError, OverlapScores, - RadixTree, WorkerId, + DumpRequest, KvIndexerInterface, KvRouterError, OverlapScores, RadixTree, WorkerId, + compute_block_hash_for_seq, }; use crate::kv_router::protocols::{ ExternalSequenceBlockHash, KvCacheEvent, KvCacheEventData, KvCacheRemoveData, KvCacheStoreData, KvCacheStoredBlockData, LocalBlockHash, }; -use crate::kv_router::RouterEvent; #[derive(Debug)] struct MatchRequest { diff --git a/lib/llm/src/kv_router/indexer.rs b/lib/llm/src/kv_router/indexer.rs index 2b3ce0a300..e9399a50d1 100644 --- a/lib/llm/src/kv_router/indexer.rs +++ b/lib/llm/src/kv_router/indexer.rs @@ -1382,10 +1382,11 @@ mod tests { let worker_0 = 0; let worker_1 = 1; - assert!(trie - .find_matches(vec![LocalBlockHash(0)], false) - .scores - .is_empty()); + assert!( + trie.find_matches(vec![LocalBlockHash(0)], false) + .scores + .is_empty() + ); trie.apply_event(create_store_event(worker_0, 0, vec![0], None)); trie.apply_event(create_store_event(worker_1, 0, vec![0], None)); @@ -1406,10 +1407,11 @@ mod tests { let worker_0 = 0; let worker_1 = 1; - assert!(trie - .find_matches(vec![LocalBlockHash(0)], false) - .scores - .is_empty()); + assert!( + trie.find_matches(vec![LocalBlockHash(0)], false) + .scores + .is_empty() + ); // Test clearing an empty worker trie.clear_all_blocks(worker_0); diff --git a/lib/llm/src/kv_router/metrics_aggregator.rs b/lib/llm/src/kv_router/metrics_aggregator.rs index 7ab4e1372c..6265518d9a 100644 --- a/lib/llm/src/kv_router/metrics_aggregator.rs +++ b/lib/llm/src/kv_router/metrics_aggregator.rs @@ -15,13 +15,13 @@ use std::sync::Once; -pub use crate::kv_router::protocols::{ForwardPassMetrics, LoadMetrics, PredictiveLoadMetrics}; use crate::kv_router::KV_METRICS_ENDPOINT; +pub use crate::kv_router::protocols::{ForwardPassMetrics, LoadMetrics, PredictiveLoadMetrics}; -use crate::kv_router::scoring::Endpoint; use crate::kv_router::ProcessedEndpoints; +use crate::kv_router::scoring::Endpoint; use dynamo_runtime::component::Component; -use dynamo_runtime::{service::EndpointInfo, utils::Duration, Result}; +use dynamo_runtime::{Result, service::EndpointInfo, utils::Duration}; use tokio::sync::watch; use tokio_util::sync::CancellationToken; diff --git a/lib/llm/src/kv_router/publisher.rs b/lib/llm/src/kv_router/publisher.rs index dbcea3cf76..073372f7a6 100644 --- a/lib/llm/src/kv_router/publisher.rs +++ b/lib/llm/src/kv_router/publisher.rs @@ -14,21 +14,21 @@ // limitations under the License. use crate::kv_router::{ - indexer::{compute_block_hash_for_seq, RouterEvent}, + KV_EVENT_SUBJECT, KV_METRICS_ENDPOINT, KV_METRICS_SUBJECT, + indexer::{RouterEvent, compute_block_hash_for_seq}, protocols::*, scoring::LoadEvent, - KV_EVENT_SUBJECT, KV_METRICS_ENDPOINT, KV_METRICS_SUBJECT, }; use async_trait::async_trait; -use dynamo_runtime::traits::{events::EventPublisher, DistributedRuntimeProvider}; +use dynamo_runtime::traits::{DistributedRuntimeProvider, events::EventPublisher}; use dynamo_runtime::{ + Error, Result, component::{Component, Namespace}, pipeline::{ - network::Ingress, AsyncEngine, AsyncEngineContextProvider, ManyOut, ResponseStream, - SingleIn, + AsyncEngine, AsyncEngineContextProvider, ManyOut, ResponseStream, SingleIn, + network::Ingress, }, protocols::annotated::Annotated, - Error, Result, }; use futures::stream; use std::sync::Arc; diff --git a/lib/llm/src/kv_router/scheduler.rs b/lib/llm/src/kv_router/scheduler.rs index f722442fe0..a0300e60bc 100644 --- a/lib/llm/src/kv_router/scheduler.rs +++ b/lib/llm/src/kv_router/scheduler.rs @@ -11,12 +11,12 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::watch; +use super::KV_HIT_RATE_SUBJECT; +use super::KvRouterConfig; +use super::WorkerSelector; use super::indexer::OverlapScores; use super::protocols::WorkerSelectionResult; use super::sequence::ActiveSequencesMultiWorker; -use super::KvRouterConfig; -use super::WorkerSelector; -use super::KV_HIT_RATE_SUBJECT; use crate::tokens::SequenceHash; @@ -293,7 +293,7 @@ fn softmax_sample(logits: &HashMap, temperature: f64) -> i64 { // Collect all keys with the minimum logit value (to handle ties) let min_keys: Vec<_> = logits .iter() - .filter(|(_, &v)| v == min_logit) + .filter(|&(_, &v)| v == min_logit) .map(|(k, _)| *k) .collect(); diff --git a/lib/llm/src/kv_router/sequence.rs b/lib/llm/src/kv_router/sequence.rs index 86d3ca66a5..e4bfe5cef3 100644 --- a/lib/llm/src/kv_router/sequence.rs +++ b/lib/llm/src/kv_router/sequence.rs @@ -29,8 +29,8 @@ use anyhow::Result; use dashmap::DashMap; use derive_getters::Getters; use dynamo_runtime::component::Component; -use dynamo_runtime::traits::events::{EventPublisher, EventSubscriber}; use dynamo_runtime::traits::DistributedRuntimeProvider; +use dynamo_runtime::traits::events::{EventPublisher, EventSubscriber}; use futures::StreamExt; use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -428,21 +428,21 @@ impl ActiveSequencesMultiWorker { } } ActiveSequenceEventData::Free => { - if let Some((_, worker_id)) = request_to_worker.remove(&event.request_id) { - if let Some(sender) = senders.get(&worker_id) { - let _ = sender.send(UpdateSequences::Free { - request_id: event.request_id.clone(), - }); - } + if let Some((_, worker_id)) = request_to_worker.remove(&event.request_id) + && let Some(sender) = senders.get(&worker_id) + { + let _ = sender.send(UpdateSequences::Free { + request_id: event.request_id.clone(), + }); } } ActiveSequenceEventData::MarkPrefillCompleted => { - if let Some(worker_id) = request_to_worker.get(&event.request_id) { - if let Some(sender) = senders.get(&*worker_id) { - let _ = sender.send(UpdateSequences::MarkPrefillCompleted { - request_id: event.request_id.clone(), - }); - } + if let Some(worker_id) = request_to_worker.get(&event.request_id) + && let Some(sender) = senders.get(&*worker_id) + { + let _ = sender.send(UpdateSequences::MarkPrefillCompleted { + request_id: event.request_id.clone(), + }); } } } diff --git a/lib/llm/src/lib.rs b/lib/llm/src/lib.rs index b76159bcf8..fbca78a7fd 100644 --- a/lib/llm/src/lib.rs +++ b/lib/llm/src/lib.rs @@ -238,9 +238,10 @@ mod file_json_field_tests { let result: anyhow::Result = file_json_field(&file_path, "non_existent_field"); assert!(result.is_err()); let err = result.unwrap_err(); - assert!(err - .to_string() - .contains("Field 'non_existent_field' not found")); + assert!( + err.to_string() + .contains("Field 'non_existent_field' not found") + ); } #[test] @@ -255,9 +256,10 @@ mod file_json_field_tests { let result: anyhow::Result = file_json_field(&file_path, "count"); assert!(result.is_err()); let err = result.unwrap_err(); - assert!(err - .to_string() - .contains("Failed to deserialize field 'count'")); + assert!( + err.to_string() + .contains("Failed to deserialize field 'count'") + ); } #[test] diff --git a/lib/llm/src/local_model.rs b/lib/llm/src/local_model.rs index ab1a0c5b91..fb5d1bbc45 100644 --- a/lib/llm/src/local_model.rs +++ b/lib/llm/src/local_model.rs @@ -263,17 +263,15 @@ impl LocalModelBuilder { } // Override runtime configs with mocker engine args - if self.is_mocker { - if let Some(path) = &self.extra_engine_args { - let mocker_engine_args = MockEngineArgs::from_json_file(path) - .expect("Failed to load mocker engine args for runtime config overriding."); - self.runtime_config.total_kv_blocks = - Some(mocker_engine_args.num_gpu_blocks as u64); - self.runtime_config.max_num_seqs = - mocker_engine_args.max_num_seqs.map(|v| v as u64); - self.runtime_config.max_num_batched_tokens = - mocker_engine_args.max_num_batched_tokens.map(|v| v as u64); - } + if self.is_mocker + && let Some(path) = &self.extra_engine_args + { + let mocker_engine_args = MockEngineArgs::from_json_file(path) + .expect("Failed to load mocker engine args for runtime config overriding."); + self.runtime_config.total_kv_blocks = Some(mocker_engine_args.num_gpu_blocks as u64); + self.runtime_config.max_num_seqs = mocker_engine_args.max_num_seqs.map(|v| v as u64); + self.runtime_config.max_num_batched_tokens = + mocker_engine_args.max_num_batched_tokens.map(|v| v as u64); } card.migration_limit = self.migration_limit; diff --git a/lib/llm/src/local_model/runtime_config.rs b/lib/llm/src/local_model/runtime_config.rs index 8c5a6a434f..b346e527d8 100644 --- a/lib/llm/src/local_model/runtime_config.rs +++ b/lib/llm/src/local_model/runtime_config.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; #[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct ModelRuntimeConfig { diff --git a/lib/llm/src/migration.rs b/lib/llm/src/migration.rs index 8ed3a24b28..7136e0d8d6 100644 --- a/lib/llm/src/migration.rs +++ b/lib/llm/src/migration.rs @@ -17,8 +17,8 @@ use crate::{ use dynamo_runtime::{ pipeline::{ - async_trait, AsyncEngineContextProvider, ManyOut, Operator, ResponseStream, - ServerStreamingEngine, SingleIn, + AsyncEngineContextProvider, ManyOut, Operator, ResponseStream, ServerStreamingEngine, + SingleIn, async_trait, }, protocols::{annotated::Annotated, maybe_error::MaybeError}, }; @@ -126,13 +126,12 @@ impl RetryManager { // TODO: Is there anything needed to pass between context? let request = SingleIn::new(self.request.clone()); response_stream = Some(self.next_generate.generate(request).await); - if let Some(err) = response_stream.as_ref().unwrap().as_ref().err() { - if let Some(req_err) = err.downcast_ref::() { - if matches!(req_err.kind(), NatsNoResponders) { - tracing::warn!("Creating new stream... retrying..."); - continue; - } - } + if let Some(err) = response_stream.as_ref().unwrap().as_ref().err() + && let Some(req_err) = err.downcast_ref::() + && matches!(req_err.kind(), NatsNoResponders) + { + tracing::warn!("Creating new stream... retrying..."); + continue; } break; } @@ -170,8 +169,8 @@ impl RetryManager { mod tests { use super::*; use crate::protocols::common::{OutputOptions, SamplingOptions, StopConditions}; - use dynamo_runtime::pipeline::context::Controller; use dynamo_runtime::pipeline::AsyncEngine; + use dynamo_runtime::pipeline::context::Controller; use std::sync::atomic::{AtomicU32, Ordering}; use tokio::sync::mpsc; @@ -624,9 +623,11 @@ mod tests { let error_response = &responses[3]; assert!(error_response.err().is_some()); if let Some(error) = error_response.err() { - assert!(error - .to_string() - .contains("Stream ended before generation completed")); + assert!( + error + .to_string() + .contains("Stream ended before generation completed") + ); } } @@ -672,9 +673,11 @@ mod tests { let error_response = &responses[3]; assert!(error_response.err().is_some()); if let Some(error) = error_response.err() { - assert!(error - .to_string() - .contains("Stream ended before generation completed")); + assert!( + error + .to_string() + .contains("Stream ended before generation completed") + ); } } } diff --git a/lib/llm/src/mocker/engine.rs b/lib/llm/src/mocker/engine.rs index d93c5095b8..d1e63f42c7 100644 --- a/lib/llm/src/mocker/engine.rs +++ b/lib/llm/src/mocker/engine.rs @@ -22,18 +22,18 @@ use crate::kv_router::publisher::WorkerMetricsPublisher; use crate::mocker::protocols::DirectRequest; use crate::mocker::protocols::{MockEngineArgs, OutputSignal}; use crate::mocker::scheduler::Scheduler; -use crate::protocols::common::llm_backend::{LLMEngineOutput, PreprocessedRequest}; use crate::protocols::TokenIdType; -use dynamo_runtime::protocols::annotated::Annotated; +use crate::protocols::common::llm_backend::{LLMEngineOutput, PreprocessedRequest}; use dynamo_runtime::DistributedRuntime; +use dynamo_runtime::protocols::annotated::Annotated; use tokio_util::sync::CancellationToken; use dynamo_runtime::{ + Result, component::Component, engine::AsyncEngineContextProvider, - pipeline::{async_trait, AsyncEngine, Error, ManyOut, ResponseStream, SingleIn}, + pipeline::{AsyncEngine, Error, ManyOut, ResponseStream, SingleIn, async_trait}, traits::DistributedRuntimeProvider, - Result, }; use crate::kv_router::protocols::{KvCacheEvent, KvCacheEventData}; @@ -43,7 +43,7 @@ use rand::Rng; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use tokio::sync::{mpsc, Mutex, OnceCell}; +use tokio::sync::{Mutex, OnceCell, mpsc}; use tokio_stream::wrappers::ReceiverStream; use uuid::Uuid; @@ -523,14 +523,14 @@ pub async fn make_mocker_engine( #[cfg(test)] mod integration_tests { use super::*; - use crate::kv_router::indexer::RouterEvent; use crate::kv_router::KV_EVENT_SUBJECT; + use crate::kv_router::indexer::RouterEvent; use crate::protocols::common::{OutputOptions, SamplingOptions, StopConditions}; use dynamo_runtime::{ + DistributedRuntime, Worker, pipeline::Context, - pipeline::{network::Ingress, PushRouter}, + pipeline::{PushRouter, network::Ingress}, traits::events::EventSubscriber, - DistributedRuntime, Worker, }; use futures::StreamExt; use tokio::time::timeout; diff --git a/lib/llm/src/mocker/kv_manager.rs b/lib/llm/src/mocker/kv_manager.rs index c16a3db7b8..84239fdbf3 100644 --- a/lib/llm/src/mocker/kv_manager.rs +++ b/lib/llm/src/mocker/kv_manager.rs @@ -102,18 +102,18 @@ impl KvManager { store: bool, parent_hash: Option, ) { - if let Some(ref tx) = self.move_block_response_tx { - if !blocks.is_empty() { - if reverse { - blocks.reverse(); - } - let response = if store { - MoveBlockResponse::Store(blocks, parent_hash) - } else { - MoveBlockResponse::Remove(blocks) - }; - tx.send(response).unwrap(); + if let Some(ref tx) = self.move_block_response_tx + && !blocks.is_empty() + { + if reverse { + blocks.reverse(); } + let response = if store { + MoveBlockResponse::Store(blocks, parent_hash) + } else { + MoveBlockResponse::Remove(blocks) + }; + tx.send(response).unwrap(); } } @@ -159,10 +159,10 @@ impl KvManager { // Now insert the new block in active blocks with reference count 1 self.active_blocks.insert(hash.clone(), 1); self.all_blocks.insert(hash.clone()); - if self.move_block_response_tx.is_some() { - if let UniqueBlock::FullBlock(stored_full_block) = hash { - blocks_stored.push(*stored_full_block); - } + if self.move_block_response_tx.is_some() + && let UniqueBlock::FullBlock(stored_full_block) = hash + { + blocks_stored.push(*stored_full_block); } } @@ -184,10 +184,10 @@ impl KvManager { assert!(self.all_blocks.remove(hash)); // Track blocks for batch sending - if self.move_block_response_tx.is_some() { - if let UniqueBlock::FullBlock(destroyed_full_block) = hash { - blocks_destroyed.push(*destroyed_full_block); - } + if self.move_block_response_tx.is_some() + && let UniqueBlock::FullBlock(destroyed_full_block) = hash + { + blocks_destroyed.push(*destroyed_full_block); } } diff --git a/lib/llm/src/mocker/protocols.rs b/lib/llm/src/mocker/protocols.rs index 5023f9c31c..70eaa95288 100644 --- a/lib/llm/src/mocker/protocols.rs +++ b/lib/llm/src/mocker/protocols.rs @@ -160,58 +160,58 @@ impl MockEngineArgs { } // Apply each extra argument to the builder - if let Some(value) = extra_args.get("num_gpu_blocks") { - if let Some(num) = value.as_u64() { - builder = builder.num_gpu_blocks(num as usize); - } + if let Some(value) = extra_args.get("num_gpu_blocks") + && let Some(num) = value.as_u64() + { + builder = builder.num_gpu_blocks(num as usize); } - if let Some(value) = extra_args.get("block_size") { - if let Some(num) = value.as_u64() { - builder = builder.block_size(num as usize); - } + if let Some(value) = extra_args.get("block_size") + && let Some(num) = value.as_u64() + { + builder = builder.block_size(num as usize); } - if let Some(value) = extra_args.get("max_num_seqs") { - if let Some(num) = value.as_u64() { - builder = builder.max_num_seqs(Some(num as usize)); - } + if let Some(value) = extra_args.get("max_num_seqs") + && let Some(num) = value.as_u64() + { + builder = builder.max_num_seqs(Some(num as usize)); } - if let Some(value) = extra_args.get("max_num_batched_tokens") { - if let Some(num) = value.as_u64() { - builder = builder.max_num_batched_tokens(Some(num as usize)); - } + if let Some(value) = extra_args.get("max_num_batched_tokens") + && let Some(num) = value.as_u64() + { + builder = builder.max_num_batched_tokens(Some(num as usize)); } - if let Some(value) = extra_args.get("enable_prefix_caching") { - if let Some(enabled) = value.as_bool() { - builder = builder.enable_prefix_caching(enabled); - } + if let Some(value) = extra_args.get("enable_prefix_caching") + && let Some(enabled) = value.as_bool() + { + builder = builder.enable_prefix_caching(enabled); } - if let Some(value) = extra_args.get("enable_chunked_prefill") { - if let Some(enabled) = value.as_bool() { - builder = builder.enable_chunked_prefill(enabled); - } + if let Some(value) = extra_args.get("enable_chunked_prefill") + && let Some(enabled) = value.as_bool() + { + builder = builder.enable_chunked_prefill(enabled); } - if let Some(value) = extra_args.get("watermark") { - if let Some(num) = value.as_f64() { - builder = builder.watermark(num); - } + if let Some(value) = extra_args.get("watermark") + && let Some(num) = value.as_f64() + { + builder = builder.watermark(num); } - if let Some(value) = extra_args.get("speedup_ratio") { - if let Some(num) = value.as_f64() { - builder = builder.speedup_ratio(num); - } + if let Some(value) = extra_args.get("speedup_ratio") + && let Some(num) = value.as_f64() + { + builder = builder.speedup_ratio(num); } - if let Some(value) = extra_args.get("dp_size") { - if let Some(num) = value.as_u64() { - builder = builder.dp_size(num as u32); - } + if let Some(value) = extra_args.get("dp_size") + && let Some(num) = value.as_u64() + { + builder = builder.dp_size(num as u32); } // Build the MockEngineArgs with either defaults or overridden values diff --git a/lib/llm/src/mocker/scheduler.rs b/lib/llm/src/mocker/scheduler.rs index b166825485..72bab3366f 100644 --- a/lib/llm/src/mocker/scheduler.rs +++ b/lib/llm/src/mocker/scheduler.rs @@ -31,15 +31,15 @@ use crate::kv_router::protocols::{ForwardPassMetrics, KvCacheEventData, KvStats, WorkerStats}; use crate::mocker::evictor::LRUEvictor; use crate::mocker::kv_manager::KvManager; -use crate::mocker::protocols::{block_response_to_kv_event, MoveBlock, OutputSignal, PrefillCost}; use crate::mocker::protocols::{DirectRequest, MockEngineArgs, MoveBlockResponse}; +use crate::mocker::protocols::{MoveBlock, OutputSignal, PrefillCost, block_response_to_kv_event}; use crate::mocker::sequence::ActiveSequence; -use crate::tokens::blocks::UniqueBlock; use crate::tokens::BlockHash; +use crate::tokens::blocks::UniqueBlock; use std::collections::HashMap; use std::collections::VecDeque; use std::sync::Arc; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; use tokio::time::Duration; use tokio_util::sync::CancellationToken; use uuid::Uuid; @@ -414,9 +414,7 @@ impl Scheduler { } // Drain KV events and forward to relay after prefill signal processing - if let (Some(ref relay_tx), Some(ref mut rx)) = - (&kv_events_tx, &mut block_resp_rx) - { + if let (Some(relay_tx), Some(rx)) = (&kv_events_tx, &mut block_resp_rx) { while let Ok(event) = rx.try_recv() { let _ = relay_tx.send(block_response_to_kv_event(event, &block_hashes)); @@ -465,9 +463,7 @@ impl Scheduler { } // Drain KV events and forward to relay after decode signal processing - if let (Some(ref relay_tx), Some(ref mut rx)) = - (&kv_events_tx, &mut block_resp_rx) - { + if let (Some(relay_tx), Some(rx)) = (&kv_events_tx, &mut block_resp_rx) { while let Ok(event) = rx.try_recv() { let _ = relay_tx .send(block_response_to_kv_event(event, &sequence.block_hashes())); @@ -663,7 +659,9 @@ fn process_signals( // Check we have a Use signal with blocks let MoveBlock::Use(blocks) = signal else { - panic!("Failed signal is Invalid. Has to fail on generation signal, but failed on {signal:?}"); + panic!( + "Failed signal is Invalid. Has to fail on generation signal, but failed on {signal:?}" + ); }; // Verify the signal contains exactly one block @@ -708,7 +706,7 @@ mod tests { #[case] enable_prefix_caching: bool, #[case] enable_chunked_prefill: bool, ) { - std::env::set_var("RUST_LOG", "debug"); + unsafe { std::env::set_var("RUST_LOG", "debug") }; let kv_capacity: usize = 500; let block_size: usize = 64; diff --git a/lib/llm/src/perf/logprobs.rs b/lib/llm/src/perf/logprobs.rs index 2b4a63d1f6..0defe57526 100644 --- a/lib/llm/src/perf/logprobs.rs +++ b/lib/llm/src/perf/logprobs.rs @@ -568,7 +568,7 @@ mod tests { type TestTokenAlternative = (&'static str, f32); type TestTokenData = (&'static str, f32, Vec); type TestTokenDataVec = Vec; - use crate::perf::{record_stream_with_context, RecordingMode, TimestampedResponse}; + use crate::perf::{RecordingMode, TimestampedResponse, record_stream_with_context}; use crate::protocols::codec::create_message_stream; use crate::protocols::convert_sse_stream; use approx::assert_abs_diff_eq; diff --git a/lib/llm/src/preprocessor.rs b/lib/llm/src/preprocessor.rs index f600d08c24..076e5c1dfa 100644 --- a/lib/llm/src/preprocessor.rs +++ b/lib/llm/src/preprocessor.rs @@ -28,21 +28,21 @@ use crate::tokenizers::Encoding; use dynamo_runtime::engine::{AsyncEngine, AsyncEngineContextProvider, ResponseStream}; use dynamo_runtime::pipeline::{ - async_trait, AsyncEngineContext, Error, ManyOut, Operator, SingleIn, + AsyncEngineContext, Error, ManyOut, Operator, SingleIn, async_trait, }; use dynamo_runtime::protocols::annotated::{Annotated, AnnotationsProvider}; use crate::protocols::{ common::{OutputOptionsProvider, SamplingOptionsProvider, StopConditionsProvider}, openai::{ + DeltaGeneratorExt, chat_completions::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse}, completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, embeddings::{NvCreateEmbeddingRequest, NvCreateEmbeddingResponse}, nvext::NvExtProvider, - DeltaGeneratorExt, }, }; -use crate::tokenizers::{traits::Tokenizer, HuggingFaceTokenizer}; +use crate::tokenizers::{HuggingFaceTokenizer, traits::Tokenizer}; use crate::preprocessor::prompt::{PromptFormatter, PromptInput, TextInput, TokenInput}; @@ -487,11 +487,7 @@ impl &self, request: SingleIn, next: Arc< - dyn AsyncEngine< - SingleIn, - ManyOut>, - Error, - >, + dyn AsyncEngine, ManyOut>, Error>, >, ) -> Result>, Error> { // unpack the request @@ -545,11 +541,7 @@ impl &self, request: SingleIn, next: Arc< - dyn AsyncEngine< - SingleIn, - ManyOut>, - Error, - >, + dyn AsyncEngine, ManyOut>, Error>, >, ) -> Result>, Error> { // unpack the request @@ -603,10 +595,10 @@ impl request: SingleIn, next: Arc< dyn AsyncEngine< - SingleIn, - ManyOut>, - Error, - >, + SingleIn, + ManyOut>, + Error, + >, >, ) -> Result>, Error> { // Unpack request diff --git a/lib/llm/src/preprocessor/prompt/template/formatters.rs b/lib/llm/src/preprocessor/prompt/template/formatters.rs index 4fe9f226cc..1b1d031ae0 100644 --- a/lib/llm/src/preprocessor/prompt/template/formatters.rs +++ b/lib/llm/src/preprocessor/prompt/template/formatters.rs @@ -15,7 +15,7 @@ use std::sync::Arc; -use super::tokcfg::{raise_exception, strftime_now, tojson, ChatTemplate}; +use super::tokcfg::{ChatTemplate, raise_exception, strftime_now, tojson}; use super::{ContextMixins, HfTokenizerConfigJsonFormatter, JinjaEnvironment}; use either::Either; use minijinja::Environment; @@ -60,7 +60,9 @@ impl HfTokenizerConfigJsonFormatter { match &chat_template.0 { Either::Left(x) => { if x.contains("add_generation_prompt") { - tracing::debug!("Chat template contains `add_generation_prompt` key. This model supports add_generation_prompt."); + tracing::debug!( + "Chat template contains `add_generation_prompt` key. This model supports add_generation_prompt." + ); supports_add_generation_prompt = Some(true); } env.add_template_owned("default", x.to_string())?; @@ -72,11 +74,15 @@ impl HfTokenizerConfigJsonFormatter { if v.contains("add_generation_prompt") { match supports_add_generation_prompt { Some(true) | None => { - tracing::debug!("Chat template contains `add_generation_prompt` key. This model supports add_generation_prompt."); + tracing::debug!( + "Chat template contains `add_generation_prompt` key. This model supports add_generation_prompt." + ); supports_add_generation_prompt = Some(true); } Some(false) => { - tracing::warn!("Not all templates contain `add_generation_prompt` key. This model does not support add_generation_prompt."); + tracing::warn!( + "Not all templates contain `add_generation_prompt` key. This model does not support add_generation_prompt." + ); } } } else { @@ -86,7 +92,9 @@ impl HfTokenizerConfigJsonFormatter { } } if env.templates().count() == 0 { - anyhow::bail!("Chat template does not contain a `tool_use` or `default` key. Please ensure it contains at least a `default` key, although `tool_use` should be specified for using tools."); + anyhow::bail!( + "Chat template does not contain a `tool_use` or `default` key. Please ensure it contains at least a `default` key, although `tool_use` should be specified for using tools." + ); } } } diff --git a/lib/llm/src/preprocessor/prompt/template/tokcfg.rs b/lib/llm/src/preprocessor/prompt/template/tokcfg.rs index 6f859585b3..570ff9ad47 100644 --- a/lib/llm/src/preprocessor/prompt/template/tokcfg.rs +++ b/lib/llm/src/preprocessor/prompt/template/tokcfg.rs @@ -21,7 +21,7 @@ use chrono::{DateTime, Local}; use either::Either; use ggus::{GGufMetaKV, GGufReader}; use memmap2::Mmap; -use minijinja::{value::Kwargs, Error, ErrorKind, Value}; +use minijinja::{Error, ErrorKind, Value, value::Kwargs}; use serde::{Deserialize, Serialize}; #[allow(dead_code)] diff --git a/lib/llm/src/protocols/codec.rs b/lib/llm/src/protocols/codec.rs index d44dc529b2..0c47a0943c 100644 --- a/lib/llm/src/protocols/codec.rs +++ b/lib/llm/src/protocols/codec.rs @@ -98,14 +98,14 @@ where fn try_from(value: Message) -> Result, Self::Error> { // determine if the message had an error - if let Some(event) = value.event.as_ref() { - if event == "error" { - let message = match &value.comments { - Some(comments) => comments.join("\n"), - None => "`event: error` detected, but no error message found".to_string(), - }; - return Err(message); - } + if let Some(event) = value.event.as_ref() + && event == "error" + { + let message = match &value.comments { + Some(comments) => comments.join("\n"), + None => "`event: error` detected, but no error message found".to_string(), + }; + return Err(message); } // try to deserialize the data to T diff --git a/lib/llm/src/protocols/common/llm_backend.rs b/lib/llm/src/protocols/common/llm_backend.rs index 1c00e837a6..6a03284187 100644 --- a/lib/llm/src/protocols/common/llm_backend.rs +++ b/lib/llm/src/protocols/common/llm_backend.rs @@ -15,8 +15,8 @@ use serde::{Deserialize, Serialize}; -pub use super::preprocessor::PreprocessedRequest; pub use super::FinishReason; +pub use super::preprocessor::PreprocessedRequest; use crate::protocols::TokenIdType; use dynamo_runtime::protocols::maybe_error::MaybeError; diff --git a/lib/llm/src/protocols/openai.rs b/lib/llm/src/protocols/openai.rs index 668d8e6933..9282c817fb 100644 --- a/lib/llm/src/protocols/openai.rs +++ b/lib/llm/src/protocols/openai.rs @@ -5,8 +5,8 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; use super::{ - common::{self, OutputOptionsProvider, SamplingOptionsProvider, StopConditionsProvider}, ContentProvider, + common::{self, OutputOptionsProvider, SamplingOptionsProvider, StopConditionsProvider}, }; use crate::protocols::openai::common_ext::CommonExtProvider; @@ -20,7 +20,7 @@ pub mod responses; pub mod validate; use validate::{ - validate_range, FREQUENCY_PENALTY_RANGE, PRESENCE_PENALTY_RANGE, TEMPERATURE_RANGE, TOP_P_RANGE, + FREQUENCY_PENALTY_RANGE, PRESENCE_PENALTY_RANGE, TEMPERATURE_RANGE, TOP_P_RANGE, validate_range, }; #[derive(Serialize, Deserialize, Debug)] @@ -147,10 +147,10 @@ impl StopConditionsProvider for T { let min_tokens = self.get_min_tokens(); let stop = self.get_stop(); - if let Some(stop) = &stop { - if stop.len() > 4 { - anyhow::bail!("stop conditions must be less than 4") - } + if let Some(stop) = &stop + && stop.len() > 4 + { + anyhow::bail!("stop conditions must be less than 4") } // Use the trait method to get ignore_eos, which handles precedence diff --git a/lib/llm/src/protocols/openai/chat_completions.rs b/lib/llm/src/protocols/openai/chat_completions.rs index b97e8d7f5a..0134e48417 100644 --- a/lib/llm/src/protocols/openai/chat_completions.rs +++ b/lib/llm/src/protocols/openai/chat_completions.rs @@ -20,11 +20,11 @@ use validator::Validate; use crate::engines::ValidateRequest; use super::{ + OpenAIOutputOptionsProvider, OpenAISamplingOptionsProvider, OpenAIStopConditionsProvider, common_ext::{CommonExt, CommonExtProvider}, nvext::NvExt, nvext::NvExtProvider, - validate, OpenAIOutputOptionsProvider, OpenAISamplingOptionsProvider, - OpenAIStopConditionsProvider, + validate, }; pub mod aggregator; diff --git a/lib/llm/src/protocols/openai/chat_completions/aggregator.rs b/lib/llm/src/protocols/openai/chat_completions/aggregator.rs index a99b3e1dda..ca0bd3849c 100644 --- a/lib/llm/src/protocols/openai/chat_completions/aggregator.rs +++ b/lib/llm/src/protocols/openai/chat_completions/aggregator.rs @@ -1,27 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// -// Licensed 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. use futures::{Stream, StreamExt}; use std::collections::HashMap; use super::{NvCreateChatCompletionResponse, NvCreateChatCompletionStreamResponse}; use crate::protocols::{ + Annotated, codec::{Message, SseCodecError}, convert_sse_stream, openai::ParsingOptions, - Annotated, }; use dynamo_parsers::tool_calling::try_tool_call_parse_aggregate; @@ -177,27 +165,26 @@ impl DeltaAggregator { // After aggregation, inspect each choice's text for tool call syntax for choice in aggregator.choices.values_mut() { - if choice.tool_calls.is_none() { - if let Ok(tool_calls) = try_tool_call_parse_aggregate( + if choice.tool_calls.is_none() + && let Ok(tool_calls) = try_tool_call_parse_aggregate( &choice.text, parsing_options.tool_call_parser.as_deref(), - ) { - if tool_calls.is_empty() { - continue; - } - for tool_call in &tool_calls { - tracing::debug!( - tool_call_id = %tool_call.id, - function_name = %tool_call.function.name, - arguments = %tool_call.function.arguments, - "Parsed structured tool call from aggregated content" - ); - } - choice.tool_calls = Some(tool_calls); - choice.text.clear(); - choice.finish_reason = - Some(dynamo_async_openai::types::FinishReason::ToolCalls); + ) + { + if tool_calls.is_empty() { + continue; + } + for tool_call in &tool_calls { + tracing::debug!( + tool_call_id = %tool_call.id, + function_name = %tool_call.function.name, + arguments = %tool_call.function.arguments, + "Parsed structured tool call from aggregated content" + ); } + choice.tool_calls = Some(tool_calls); + choice.text.clear(); + choice.finish_reason = Some(dynamo_async_openai::types::FinishReason::ToolCalls); } } diff --git a/lib/llm/src/protocols/openai/completions.rs b/lib/llm/src/protocols/openai/completions.rs index a2250c8843..00fd7b1b7e 100644 --- a/lib/llm/src/protocols/openai/completions.rs +++ b/lib/llm/src/protocols/openai/completions.rs @@ -21,11 +21,12 @@ use validator::Validate; use crate::engines::ValidateRequest; use super::{ + ContentProvider, OpenAIOutputOptionsProvider, OpenAISamplingOptionsProvider, + OpenAIStopConditionsProvider, common::{self, OutputOptionsProvider, SamplingOptionsProvider, StopConditionsProvider}, common_ext::{CommonExt, CommonExtProvider}, nvext::{NvExt, NvExtProvider}, - validate, ContentProvider, OpenAIOutputOptionsProvider, OpenAISamplingOptionsProvider, - OpenAIStopConditionsProvider, + validate, }; mod aggregator; @@ -87,12 +88,11 @@ impl NvExtProvider for NvCreateCompletionRequest { } fn raw_prompt(&self) -> Option { - if let Some(nvext) = self.nvext.as_ref() { - if let Some(use_raw_prompt) = nvext.use_raw_prompt { - if use_raw_prompt { - return Some(prompt_to_string(&self.inner.prompt)); - } - } + if let Some(nvext) = self.nvext.as_ref() + && let Some(use_raw_prompt) = nvext.use_raw_prompt + && use_raw_prompt + { + return Some(prompt_to_string(&self.inner.prompt)); } None } diff --git a/lib/llm/src/protocols/openai/completions/aggregator.rs b/lib/llm/src/protocols/openai/completions/aggregator.rs index e72fc072c2..870f345b33 100644 --- a/lib/llm/src/protocols/openai/completions/aggregator.rs +++ b/lib/llm/src/protocols/openai/completions/aggregator.rs @@ -1,17 +1,5 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// -// Licensed 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. use std::collections::HashMap; @@ -20,11 +8,11 @@ use futures::{Stream, StreamExt}; use super::NvCreateCompletionResponse; use crate::protocols::{ + Annotated, DataStream, codec::{Message, SseCodecError}, common::FinishReason, convert_sse_stream, openai::ParsingOptions, - Annotated, DataStream, }; /// Aggregates a stream of [`CompletionResponse`]s into a single [`CompletionResponse`]. diff --git a/lib/llm/src/protocols/openai/embeddings/aggregator.rs b/lib/llm/src/protocols/openai/embeddings/aggregator.rs index caebc21268..3af917f0c3 100644 --- a/lib/llm/src/protocols/openai/embeddings/aggregator.rs +++ b/lib/llm/src/protocols/openai/embeddings/aggregator.rs @@ -15,8 +15,9 @@ use super::NvCreateEmbeddingResponse; use crate::protocols::{ + Annotated, codec::{Message, SseCodecError}, - convert_sse_stream, Annotated, + convert_sse_stream, }; use dynamo_runtime::engine::DataStream; @@ -71,24 +72,23 @@ impl DeltaAggregator { } }; - if aggregator.error.is_none() { - if let Some(response) = delta.data { - // For embeddings, we typically expect a single complete response - // or we accumulate data from multiple responses - match &mut aggregator.response { - Some(existing) => { - // Merge embedding data if we have multiple responses - existing.inner.data.extend(response.inner.data); - - // Update usage statistics - existing.inner.usage.prompt_tokens += - response.inner.usage.prompt_tokens; - existing.inner.usage.total_tokens += - response.inner.usage.total_tokens; - } - None => { - aggregator.response = Some(response); - } + if aggregator.error.is_none() + && let Some(response) = delta.data + { + // For embeddings, we typically expect a single complete response + // or we accumulate data from multiple responses + match &mut aggregator.response { + Some(existing) => { + // Merge embedding data if we have multiple responses + existing.inner.data.extend(response.inner.data); + + // Update usage statistics + existing.inner.usage.prompt_tokens += + response.inner.usage.prompt_tokens; + existing.inner.usage.total_tokens += response.inner.usage.total_tokens; + } + None => { + aggregator.response = Some(response); } } } diff --git a/lib/llm/src/protocols/openai/validate.rs b/lib/llm/src/protocols/openai/validate.rs index 8e196ee084..3897190687 100644 --- a/lib/llm/src/protocols/openai/validate.rs +++ b/lib/llm/src/protocols/openai/validate.rs @@ -93,30 +93,30 @@ pub const MAX_PROMPT_TOKEN_ID: u32 = 50256; /// Validates the temperature parameter pub fn validate_temperature(temperature: Option) -> Result<(), anyhow::Error> { - if let Some(temp) = temperature { - if !(MIN_TEMPERATURE..=MAX_TEMPERATURE).contains(&temp) { - anyhow::bail!( - "Temperature must be between {} and {}, got {}", - MIN_TEMPERATURE, - MAX_TEMPERATURE, - temp - ); - } + if let Some(temp) = temperature + && !(MIN_TEMPERATURE..=MAX_TEMPERATURE).contains(&temp) + { + anyhow::bail!( + "Temperature must be between {} and {}, got {}", + MIN_TEMPERATURE, + MAX_TEMPERATURE, + temp + ); } Ok(()) } /// Validates the top_p parameter pub fn validate_top_p(top_p: Option) -> Result<(), anyhow::Error> { - if let Some(p) = top_p { - if !(MIN_TOP_P..=MAX_TOP_P).contains(&p) { - anyhow::bail!( - "Top_p must be between {} and {}, got {}", - MIN_TOP_P, - MAX_TOP_P, - p - ); - } + if let Some(p) = top_p + && !(MIN_TOP_P..=MAX_TOP_P).contains(&p) + { + anyhow::bail!( + "Top_p must be between {} and {}, got {}", + MIN_TOP_P, + MAX_TOP_P, + p + ); } Ok(()) } @@ -136,30 +136,30 @@ pub fn validate_temperature_top_p_exclusion( /// Validates frequency penalty parameter pub fn validate_frequency_penalty(frequency_penalty: Option) -> Result<(), anyhow::Error> { - if let Some(penalty) = frequency_penalty { - if !(MIN_FREQUENCY_PENALTY..=MAX_FREQUENCY_PENALTY).contains(&penalty) { - anyhow::bail!( - "Frequency penalty must be between {} and {}, got {}", - MIN_FREQUENCY_PENALTY, - MAX_FREQUENCY_PENALTY, - penalty - ); - } + if let Some(penalty) = frequency_penalty + && !(MIN_FREQUENCY_PENALTY..=MAX_FREQUENCY_PENALTY).contains(&penalty) + { + anyhow::bail!( + "Frequency penalty must be between {} and {}, got {}", + MIN_FREQUENCY_PENALTY, + MAX_FREQUENCY_PENALTY, + penalty + ); } Ok(()) } /// Validates presence penalty parameter pub fn validate_presence_penalty(presence_penalty: Option) -> Result<(), anyhow::Error> { - if let Some(penalty) = presence_penalty { - if !(MIN_PRESENCE_PENALTY..=MAX_PRESENCE_PENALTY).contains(&penalty) { - anyhow::bail!( - "Presence penalty must be between {} and {}, got {}", - MIN_PRESENCE_PENALTY, - MAX_PRESENCE_PENALTY, - penalty - ); - } + if let Some(penalty) = presence_penalty + && !(MIN_PRESENCE_PENALTY..=MAX_PRESENCE_PENALTY).contains(&penalty) + { + anyhow::bail!( + "Presence penalty must be between {} and {}, got {}", + MIN_PRESENCE_PENALTY, + MAX_PRESENCE_PENALTY, + penalty + ); } Ok(()) } @@ -197,10 +197,10 @@ pub fn validate_logit_bias( /// Validates n parameter (number of choices) pub fn validate_n(n: Option) -> Result<(), anyhow::Error> { - if let Some(value) = n { - if !(MIN_N..=MAX_N).contains(&value) { - anyhow::bail!("n must be between {} and {}, got {}", MIN_N, MAX_N, value); - } + if let Some(value) = n + && !(MIN_N..=MAX_N).contains(&value) + { + anyhow::bail!("n must be between {} and {}, got {}", MIN_N, MAX_N, value); } Ok(()) } @@ -215,10 +215,10 @@ pub fn validate_model(model: &str) -> Result<(), anyhow::Error> { /// Validates user parameter pub fn validate_user(user: Option<&str>) -> Result<(), anyhow::Error> { - if let Some(user_id) = user { - if user_id.trim().is_empty() { - anyhow::bail!("User ID cannot be empty"); - } + if let Some(user_id) = user + && user_id.trim().is_empty() + { + anyhow::bail!("User ID cannot be empty"); } Ok(()) } @@ -270,14 +270,14 @@ pub fn validate_messages( /// Validates top_logprobs parameter pub fn validate_top_logprobs(top_logprobs: Option) -> Result<(), anyhow::Error> { - if let Some(value) = top_logprobs { - if !(0..=20).contains(&value) { - anyhow::bail!( - "Top_logprobs must be between 0 and {}, got {}", - MAX_TOP_LOGPROBS, - value - ); - } + if let Some(value) = top_logprobs + && !(0..=20).contains(&value) + { + anyhow::bail!( + "Top_logprobs must be between 0 and {}, got {}", + MAX_TOP_LOGPROBS, + value + ); } Ok(()) } @@ -340,14 +340,14 @@ pub fn validate_metadata(metadata: &Option) -> Result<(), any ); } - if let Some(value_str) = value.as_str() { - if value_str.len() > MAX_METADATA_VALUE_LENGTH { - anyhow::bail!( - "Metadata value for key '{}' exceeds {} character limit", - key, - MAX_METADATA_VALUE_LENGTH - ); - } + if let Some(value_str) = value.as_str() + && value_str.len() > MAX_METADATA_VALUE_LENGTH + { + anyhow::bail!( + "Metadata value for key '{}' exceeds {} character limit", + key, + MAX_METADATA_VALUE_LENGTH + ); } } } @@ -438,14 +438,14 @@ pub fn validate_prompt(prompt: &dynamo_async_openai::types::Prompt) -> Result<() /// Validates logprobs parameter (for completion requests) pub fn validate_logprobs(logprobs: Option) -> Result<(), anyhow::Error> { - if let Some(value) = logprobs { - if !(MIN_LOGPROBS..=MAX_LOGPROBS).contains(&value) { - anyhow::bail!( - "Logprobs must be between 0 and {}, got {}", - MAX_LOGPROBS, - value - ); - } + if let Some(value) = logprobs + && !(MIN_LOGPROBS..=MAX_LOGPROBS).contains(&value) + { + anyhow::bail!( + "Logprobs must be between 0 and {}, got {}", + MAX_LOGPROBS, + value + ); } Ok(()) } @@ -461,14 +461,14 @@ pub fn validate_best_of(best_of: Option, n: Option) -> Result<(), anyhow ); } - if let Some(n_value) = n { - if best_of_value < n_value { - anyhow::bail!( - "Best_of must be greater than or equal to n, got best_of={} and n={}", - best_of_value, - n_value - ); - } + if let Some(n_value) = n + && best_of_value < n_value + { + anyhow::bail!( + "Best_of must be greater than or equal to n, got best_of={} and n={}", + best_of_value, + n_value + ); } } Ok(()) @@ -487,10 +487,10 @@ pub fn validate_suffix(suffix: Option<&str>) -> Result<(), anyhow::Error> { /// Validates max_tokens parameter pub fn validate_max_tokens(max_tokens: Option) -> Result<(), anyhow::Error> { - if let Some(tokens) = max_tokens { - if tokens == 0 { - anyhow::bail!("Max tokens must be greater than 0, got {}", tokens); - } + if let Some(tokens) = max_tokens + && tokens == 0 + { + anyhow::bail!("Max tokens must be greater than 0, got {}", tokens); } Ok(()) } @@ -499,13 +499,13 @@ pub fn validate_max_tokens(max_tokens: Option) -> Result<(), anyhow::Error> pub fn validate_max_completion_tokens( max_completion_tokens: Option, ) -> Result<(), anyhow::Error> { - if let Some(tokens) = max_completion_tokens { - if tokens == 0 { - anyhow::bail!( - "Max completion tokens must be greater than 0, got {}", - tokens - ); - } + if let Some(tokens) = max_completion_tokens + && tokens == 0 + { + anyhow::bail!( + "Max completion tokens must be greater than 0, got {}", + tokens + ); } Ok(()) } diff --git a/lib/llm/src/recorder.rs b/lib/llm/src/recorder.rs index 9e52d54cf3..873fa7b221 100644 --- a/lib/llm/src/recorder.rs +++ b/lib/llm/src/recorder.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::fs::{self, File, OpenOptions}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; use tokio_util::sync::CancellationToken; /// Record entry that will be serialized to JSONL @@ -70,10 +70,10 @@ where let first_event_time_clone = first_event_time.clone(); // Ensure the directory exists - if let Some(parent) = output_path.as_ref().parent() { - if !parent.exists() { - fs::create_dir_all(parent).await?; - } + if let Some(parent) = output_path.as_ref().parent() + && !parent.exists() + { + fs::create_dir_all(parent).await?; } // Create the file for writing @@ -102,16 +102,16 @@ where loop { // Check time limit if set - if let Some(deadline) = max_time_deadline { - if Instant::now() >= deadline { - tracing::info!("Recorder reached max time limit, shutting down"); - // Flush and cancel - if let Err(e) = writer.flush().await { - tracing::error!("Failed to flush on time limit shutdown: {}", e); - } - cancel_clone.cancel(); - return; + if let Some(deadline) = max_time_deadline + && Instant::now() >= deadline + { + tracing::info!("Recorder reached max time limit, shutting down"); + // Flush and cancel + if let Err(e) = writer.flush().await { + tracing::error!("Failed to flush on time limit shutdown: {}", e); } + cancel_clone.cancel(); + return; } tokio::select! { @@ -170,8 +170,8 @@ where line_count += 1; // Check if we need to rotate to a new file - if let Some(max_lines) = max_lines_per_file { - if line_count >= max_lines { + if let Some(max_lines) = max_lines_per_file + && line_count >= max_lines { // Flush the current file if let Err(e) = writer.flush().await { tracing::error!("Failed to flush file before rotation: {}", e); @@ -200,15 +200,14 @@ where } } } - } // Update event count let mut count = event_count_clone.lock().await; *count += 1; // Check if we've reached the maximum count - if let Some(max) = max_count { - if *count >= max { + if let Some(max) = max_count + && *count >= max { tracing::info!("Recorder reached max event count ({}), shutting down", max); // Flush buffer before shutting down if let Err(e) = writer.flush().await { @@ -219,7 +218,6 @@ where cancel_clone.cancel(); return; } - } } } } @@ -307,19 +305,19 @@ where // Read and send events line by line while let Some(line) = lines.next_line().await? { // Check if we've reached the maximum count - if let Some(max) = max_count { - if count >= max { - tracing::info!("Reached maximum event count ({}), stopping", max); - break; - } + if let Some(max) = max_count + && count >= max + { + tracing::info!("Reached maximum event count ({}), stopping", max); + break; } // Check if we've exceeded the time limit - if let Some(end_time) = deadline { - if Instant::now() >= end_time { - tracing::info!("Reached maximum time limit, stopping"); - break; - } + if let Some(end_time) = deadline + && Instant::now() >= end_time + { + tracing::info!("Reached maximum time limit, stopping"); + break; } line_number += 1; @@ -346,12 +344,12 @@ where let event = record.event; // Handle timing if needed - if timed && prev_timestamp.is_some() { - let prev = prev_timestamp.unwrap(); - if timestamp > prev { - let wait_time = timestamp - prev; - tokio::time::sleep(Duration::from_millis(wait_time)).await; - } + if timed + && let Some(prev) = prev_timestamp + && timestamp > prev + { + let wait_time = timestamp - prev; + tokio::time::sleep(Duration::from_millis(wait_time)).await; } // Send the event @@ -612,7 +610,10 @@ mod tests { // Should have MAX_LINES_PER_FILE lines in each file (except maybe the last one) if i < found_files.len() - 1 { - assert_eq!(line_count, MAX_LINES_PER_FILE, "Each file except possibly the last should have exactly MAX_LINES_PER_FILE lines"); + assert_eq!( + line_count, MAX_LINES_PER_FILE, + "Each file except possibly the last should have exactly MAX_LINES_PER_FILE lines" + ); } total_lines += line_count; @@ -631,19 +632,19 @@ mod tests { line_number += 1; let entry: RecordEntry = serde_json::from_str(&line).unwrap(); - if let Some(prev) = prev_timestamp { - if entry.timestamp < prev { - unsorted_count += 1; - if unsorted_count <= 5 { - // Only log first 5 violations to avoid spam - println!( - "Timestamp order violation in file {} at line {}: {} < {}", - file_path.display(), - line_number, - entry.timestamp, - prev - ); - } + if let Some(prev) = prev_timestamp + && entry.timestamp < prev + { + unsorted_count += 1; + if unsorted_count <= 5 { + // Only log first 5 violations to avoid spam + println!( + "Timestamp order violation in file {} at line {}: {} < {}", + file_path.display(), + line_number, + entry.timestamp, + prev + ); } } diff --git a/lib/llm/src/tokenizers/hf.rs b/lib/llm/src/tokenizers/hf.rs index 38b8825458..ad39cf371a 100644 --- a/lib/llm/src/tokenizers/hf.rs +++ b/lib/llm/src/tokenizers/hf.rs @@ -16,8 +16,8 @@ use tokenizers::tokenizer::Tokenizer as HfTokenizer; use super::{ - traits::{Decoder, Encoder, Tokenizer}, Encoding, Error, Result, TokenIdType, + traits::{Decoder, Encoder, Tokenizer}, }; pub struct HuggingFaceTokenizer { diff --git a/lib/llm/src/tokens.rs b/lib/llm/src/tokens.rs index edc9a1a466..e51156c3f4 100644 --- a/lib/llm/src/tokens.rs +++ b/lib/llm/src/tokens.rs @@ -603,7 +603,11 @@ impl TokenBlockSequence { Some(range) => { // Since we only added one token, the range can only be empty or have one element. // If it's not empty, it must be `n..(n+1)`. - assert_eq!(range.len(), 1, "Appending a single token completed more than one block, which should be impossible."); + assert_eq!( + range.len(), + 1, + "Appending a single token completed more than one block, which should be impossible." + ); Ok(Some(range.start)) } } @@ -1108,7 +1112,7 @@ mod tests { let tokens2 = Tokens::from(vec![5, 6, 7, 8]); let chunk2 = TokenBlockChunk::new(tokens2.clone(), salt); let block2 = TokenBlock::from_chunk(chunk2, block1.parent_sequence_hash()); // Incorrect parent - // Sequence hash should differ if parent is wrong + // Sequence hash should differ if parent is wrong assert_ne!(block2.sequence_hash(), SEQ_HASH_5_8); let chunk2_correct = TokenBlockChunk::new(tokens2.clone(), salt); diff --git a/lib/llm/tests/aggregators.rs b/lib/llm/tests/aggregators.rs index c6ad39dfa9..9f58185b70 100644 --- a/lib/llm/tests/aggregators.rs +++ b/lib/llm/tests/aggregators.rs @@ -14,13 +14,13 @@ // limitations under the License. use dynamo_llm::protocols::{ - codec::{create_message_stream, Message, SseCodecError}, + ContentProvider, DataStream, + codec::{Message, SseCodecError, create_message_stream}, openai::{ - chat_completions::{aggregator::ChatCompletionAggregator, NvCreateChatCompletionResponse}, - completions::NvCreateCompletionResponse, ParsingOptions, + chat_completions::{NvCreateChatCompletionResponse, aggregator::ChatCompletionAggregator}, + completions::NvCreateCompletionResponse, }, - ContentProvider, DataStream, }; use futures::StreamExt; diff --git a/lib/llm/tests/block_manager.rs b/lib/llm/tests/block_manager.rs index 397134e46c..44c82ac989 100644 --- a/lib/llm/tests/block_manager.rs +++ b/lib/llm/tests/block_manager.rs @@ -46,10 +46,10 @@ pub mod llm_kvbm { }, }; use dynamo_llm::tokens::{BlockHash, SequenceHash}; + use dynamo_runtime::DistributedRuntime; use dynamo_runtime::component::Namespace; use dynamo_runtime::prelude::DistributedRuntimeProvider; use dynamo_runtime::traits::events::EventPublisher; - use dynamo_runtime::DistributedRuntime; use kvbm::events::EventManager; use tokio::sync::mpsc; pub use tokio_util::sync::CancellationToken; @@ -383,18 +383,18 @@ mod tests { use dynamo_llm::tokens::{TokenBlockSequence, Tokens}; use dynamo_runtime::{ - traits::events::{EventPublisher, EventSubscriber}, DistributedRuntime, Runtime, + traits::events::{EventPublisher, EventSubscriber}, }; use kvbm::{ - block::registry::BlockRegistry, - block::state::CompleteState, + KvBlockManagerConfig, KvManagerLayoutConfig, KvManagerModelConfig, NixlOptions, + ReferenceBlockManager, block::BlockState, block::GlobalRegistry, + block::registry::BlockRegistry, + block::state::CompleteState, events::EventManager, storage::{DeviceAllocator, DiskAllocator, PinnedAllocator}, - KvBlockManagerConfig, KvManagerLayoutConfig, KvManagerModelConfig, NixlOptions, - ReferenceBlockManager, }; use dynamo_llm::kv_router::{ diff --git a/lib/llm/tests/http-service.rs b/lib/llm/tests/http-service.rs index 0e122313f0..5c7ffc0b51 100644 --- a/lib/llm/tests/http-service.rs +++ b/lib/llm/tests/http-service.rs @@ -21,30 +21,30 @@ use dynamo_llm::http::{ GenericBYOTClient, HttpClientConfig, HttpRequestContext, NvCustomClient, PureOpenAIClient, }, service::{ + Metrics, error::HttpError, - metrics::{Endpoint, RequestType, Status, FRONTEND_METRIC_PREFIX}, + metrics::{Endpoint, FRONTEND_METRIC_PREFIX, RequestType, Status}, service_v2::HttpService, - Metrics, }, }; use dynamo_llm::protocols::{ + Annotated, codec::SseLineCodec, convert_sse_stream, openai::{ chat_completions::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse}, completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, }, - Annotated, }; use dynamo_runtime::{ + CancellationToken, engine::AsyncEngineContext, pipeline::{ - async_trait, AsyncEngine, AsyncEngineContextProvider, ManyOut, ResponseStream, SingleIn, + AsyncEngine, AsyncEngineContextProvider, ManyOut, ResponseStream, SingleIn, async_trait, }, - CancellationToken, }; use futures::StreamExt; -use prometheus::{proto::MetricType, Registry}; +use prometheus::{Registry, proto::MetricType}; use reqwest::StatusCode; use std::{io::Cursor, sync::Arc}; use tokio::time::timeout; @@ -1232,23 +1232,23 @@ async fn test_request_id_annotation() { let mut annotated_stream = std::pin::pin!(annotated_stream); while let Some(annotated_response) = annotated_stream.next().await { // Check if this is a request_id annotation - if let Some(event) = &annotated_response.event { - if event == "request_id" { - found_request_id_annotation = true; - // Extract the request ID from the annotation - if let Some(comments) = &annotated_response.comment { - if let Some(comment) = comments.first() { - // The comment contains a JSON-encoded string, so we need to parse it - if let Ok(parsed_value) = serde_json::from_str::(comment) { - received_request_id = Some(parsed_value); - } else { - // Fallback: remove quotes manually if JSON parsing fails - received_request_id = Some(comment.trim_matches('"').to_string()); - } - } + if let Some(event) = &annotated_response.event + && event == "request_id" + { + found_request_id_annotation = true; + // Extract the request ID from the annotation + if let Some(comments) = &annotated_response.comment + && let Some(comment) = comments.first() + { + // The comment contains a JSON-encoded string, so we need to parse it + if let Ok(parsed_value) = serde_json::from_str::(comment) { + received_request_id = Some(parsed_value); + } else { + // Fallback: remove quotes manually if JSON parsing fails + received_request_id = Some(comment.trim_matches('"').to_string()); } - break; } + break; } } diff --git a/lib/llm/tests/http_metrics.rs b/lib/llm/tests/http_metrics.rs index 925e5b785f..160be08e3e 100644 --- a/lib/llm/tests/http_metrics.rs +++ b/lib/llm/tests/http_metrics.rs @@ -15,7 +15,7 @@ use ports::get_random_port; #[serial] async fn metrics_prefix_default_then_env_override() { // Case 1: default prefix - env::remove_var(metrics::METRICS_PREFIX_ENV); + unsafe { env::remove_var(metrics::METRICS_PREFIX_ENV) }; let p1 = get_random_port().await; let svc1 = HttpService::builder().port(p1).build().unwrap(); let token1 = CancellationToken::new(); @@ -42,7 +42,7 @@ async fn metrics_prefix_default_then_env_override() { let _ = h1.await; // ensure port is released // Case 2: env override to prefix - env::set_var(metrics::METRICS_PREFIX_ENV, "custom_prefix"); + unsafe { env::set_var(metrics::METRICS_PREFIX_ENV, "custom_prefix") }; let p2 = get_random_port().await; let svc2 = HttpService::builder().port(p2).build().unwrap(); let token2 = CancellationToken::new(); @@ -69,7 +69,7 @@ async fn metrics_prefix_default_then_env_override() { let _ = h2.await; // Case 3: invalid env prefix is sanitized - env::set_var(metrics::METRICS_PREFIX_ENV, "nv-llm/http service"); + unsafe { env::set_var(metrics::METRICS_PREFIX_ENV, "nv-llm/http service") }; let p3 = get_random_port().await; let svc3 = HttpService::builder().port(p3).build().unwrap(); let token3 = CancellationToken::new(); @@ -94,7 +94,7 @@ async fn metrics_prefix_default_then_env_override() { let _ = h3.await; // Cleanup env to avoid leaking state - env::remove_var(metrics::METRICS_PREFIX_ENV); + unsafe { env::remove_var(metrics::METRICS_PREFIX_ENV) }; } // Poll /metrics until ready or timeout diff --git a/lib/llm/tests/logprob_analysis_integration.rs b/lib/llm/tests/logprob_analysis_integration.rs index c3b175fd2f..7ef87d4cdc 100644 --- a/lib/llm/tests/logprob_analysis_integration.rs +++ b/lib/llm/tests/logprob_analysis_integration.rs @@ -235,8 +235,8 @@ fn create_multi_choice_stream() -> Arc Arc> { +fn create_stream_with_multiple_close_tokens() +-> Arc> { let start_time = Instant::now(); let responses = vec![TimestampedResponse::new( create_response_with_linear_probs( diff --git a/lib/llm/tests/preprocessor.rs b/lib/llm/tests/preprocessor.rs index 6b69ba05d1..ac7bdbaa91 100644 --- a/lib/llm/tests/preprocessor.rs +++ b/lib/llm/tests/preprocessor.rs @@ -8,7 +8,7 @@ use dynamo_llm::preprocessor::prompt::PromptFormatter; use dynamo_llm::protocols::openai::chat_completions::NvCreateChatCompletionRequest; use serde::{Deserialize, Serialize}; -use hf_hub::{api::tokio::ApiBuilder, Cache, Repo, RepoType}; +use hf_hub::{Cache, Repo, RepoType, api::tokio::ApiBuilder}; use std::path::PathBuf; diff --git a/lib/llm/tests/test_common_ext.rs b/lib/llm/tests/test_common_ext.rs index d3d5307d4a..a497a67629 100644 --- a/lib/llm/tests/test_common_ext.rs +++ b/lib/llm/tests/test_common_ext.rs @@ -132,7 +132,7 @@ fn test_chat_completions_common_overrides_nvext() { Some(true) ); assert_eq!(request.get_guided_regex(), Some(".*".to_string())); // common value takes precedence - // Verify precedence through stop conditions extraction + // Verify precedence through stop conditions extraction let stop_conditions = request.extract_stop_conditions().unwrap(); assert_eq!(stop_conditions.ignore_eos, Some(false)); // common value takes precedence assert_eq!(stop_conditions.min_tokens, Some(50)); diff --git a/lib/parsers/src/tool_calling/mod.rs b/lib/parsers/src/tool_calling/mod.rs index 2568e3e416..3c8397ae90 100644 --- a/lib/parsers/src/tool_calling/mod.rs +++ b/lib/parsers/src/tool_calling/mod.rs @@ -8,10 +8,10 @@ pub mod tools; // Re-export main types and functions for convenience pub use json_parser::{ - try_tool_call_parse_json, CalledFunctionArguments, CalledFunctionParameters, + CalledFunctionArguments, CalledFunctionParameters, try_tool_call_parse_json, }; pub use parsers::{ - detect_and_parse_tool_call, JsonParserConfig, ToolCallConfig, ToolCallParserType, + JsonParserConfig, ToolCallConfig, ToolCallParserType, detect_and_parse_tool_call, }; pub use response::{CalledFunction, ToolCallResponse, ToolCallType}; pub use tools::{try_tool_call_parse_aggregate, try_tool_call_parse_stream}; diff --git a/lib/parsers/src/tool_calling/parsers.rs b/lib/parsers/src/tool_calling/parsers.rs index 471b90d059..c703f7e03d 100644 --- a/lib/parsers/src/tool_calling/parsers.rs +++ b/lib/parsers/src/tool_calling/parsers.rs @@ -839,8 +839,8 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it } #[test] - fn test_detect_and_parse_tool_call_default_parser_llama3_json_without_python_tag_multiple_with_new_lines( - ) { + fn test_detect_and_parse_tool_call_default_parser_llama3_json_without_python_tag_multiple_with_new_lines() + { let input = r#" {"name": "get_weather", "arguments": {"location": "San Francisco, CA", diff --git a/lib/parsers/src/tool_calling/tools.rs b/lib/parsers/src/tool_calling/tools.rs index 7f326b46ad..284f8d751b 100644 --- a/lib/parsers/src/tool_calling/tools.rs +++ b/lib/parsers/src/tool_calling/tools.rs @@ -5,7 +5,7 @@ pub use super::response::*; // Import json_parser from postprocessor module pub use super::json_parser::*; -pub use super::parsers::{detect_and_parse_tool_call, ToolCallConfig}; +pub use super::parsers::{ToolCallConfig, detect_and_parse_tool_call}; /// Try parsing a string as a structured tool call, for aggregation usage. /// diff --git a/lib/runtime/examples/Cargo.lock b/lib/runtime/examples/Cargo.lock index 31278cc7d8..e8efd340df 100644 --- a/lib/runtime/examples/Cargo.lock +++ b/lib/runtime/examples/Cargo.lock @@ -268,6 +268,15 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -658,6 +667,7 @@ dependencies = [ "async-trait", "async_zmq", "axum", + "bincode", "blake3", "bytes", "chrono", diff --git a/lib/runtime/examples/rust-toolchain.toml b/lib/runtime/examples/rust-toolchain.toml index b8889a3bb3..b67e7d5348 100644 --- a/lib/runtime/examples/rust-toolchain.toml +++ b/lib/runtime/examples/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.87.0" +channel = "1.89.0" diff --git a/lib/runtime/src/component.rs b/lib/runtime/src/component.rs index 0c00c87143..157d11a279 100644 --- a/lib/runtime/src/component.rs +++ b/lib/runtime/src/component.rs @@ -32,21 +32,20 @@ use crate::{ config::HealthStatus, discovery::Lease, - metrics::{prometheus_names, MetricsRegistry}, + metrics::{MetricsRegistry, prometheus_names}, service::ServiceSet, transports::etcd::EtcdPath, }; use super::{ - error, + DistributedRuntime, Result, Runtime, error, traits::*, transports::etcd::{COMPONENT_KEYWORD, ENDPOINT_KEYWORD}, transports::nats::Slug, utils::Duration, - DistributedRuntime, Result, Runtime, }; -use crate::pipeline::network::{ingress::push_endpoint::PushEndpoint, PushWorkHandler}; +use crate::pipeline::network::{PushWorkHandler, ingress::push_endpoint::PushEndpoint}; use crate::protocols::EndpointId; use crate::service::ComponentNatsServerPrometheusMetrics; use async_nats::{ @@ -288,11 +287,13 @@ impl Component { let component_clone = self.clone(); let mut hierarchies = self.parent_hierarchy(); hierarchies.push(self.hierarchy()); - debug_assert!(hierarchies - .last() - .map(|x| x.as_str()) - .unwrap_or_default() - .eq_ignore_ascii_case(&self.service_name())); // it happens that in component, hierarchy and service name are the same + debug_assert!( + hierarchies + .last() + .map(|x| x.as_str()) + .unwrap_or_default() + .eq_ignore_ascii_case(&self.service_name()) + ); // it happens that in component, hierarchy and service name are the same // Start a background task that scrapes stats every 5 seconds let m = component_metrics.clone(); diff --git a/lib/runtime/src/component/endpoint.rs b/lib/runtime/src/component/endpoint.rs index 1ef6ad0d49..98a9a15f19 100644 --- a/lib/runtime/src/component/endpoint.rs +++ b/lib/runtime/src/component/endpoint.rs @@ -148,19 +148,18 @@ impl EndpointConfigBuilder { let info = serde_json::to_vec_pretty(&info)?; - if let Some(etcd_client) = &endpoint.component.drt.etcd_client { - if let Err(e) = etcd_client + if let Some(etcd_client) = &endpoint.component.drt.etcd_client + && let Err(e) = etcd_client .kv_create( &endpoint.etcd_path_with_lease_id(lease_id), info, Some(lease_id), ) .await - { - tracing::error!("Failed to register discoverable service: {:?}", e); - cancel_token.cancel(); - return Err(error!("Failed to register discoverable service")); - } + { + tracing::error!("Failed to register discoverable service: {:?}", e); + cancel_token.cancel(); + return Err(error!("Failed to register discoverable service")); } task.await??; diff --git a/lib/runtime/src/config.rs b/lib/runtime/src/config.rs index f88d75cd66..33ebb676dd 100644 --- a/lib/runtime/src/config.rs +++ b/lib/runtime/src/config.rs @@ -4,8 +4,8 @@ use super::Result; use derive_builder::Builder; use figment::{ - providers::{Env, Format, Serialized, Toml}, Figment, + providers::{Env, Format, Serialized, Toml}, }; use serde::{Deserialize, Serialize}; use std::fmt; @@ -371,12 +371,14 @@ mod tests { let result = RuntimeConfig::from_settings(); assert!(result.is_err()); if let Err(e) = result { - assert!(e - .to_string() - .contains("num_worker_threads: Validation error")); - assert!(e - .to_string() - .contains("max_blocking_threads: Validation error")); + assert!( + e.to_string() + .contains("num_worker_threads: Validation error") + ); + assert!( + e.to_string() + .contains("max_blocking_threads: Validation error") + ); } Ok(()) }, diff --git a/lib/runtime/src/discovery.rs b/lib/runtime/src/discovery.rs index a68c046e91..57c6c9d091 100644 --- a/lib/runtime/src/discovery.rs +++ b/lib/runtime/src/discovery.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{transports::etcd, Result}; +use crate::{Result, transports::etcd}; pub use etcd::Lease; diff --git a/lib/runtime/src/distributed.rs b/lib/runtime/src/distributed.rs index d30300168e..e8bd6789f3 100644 --- a/lib/runtime/src/distributed.rs +++ b/lib/runtime/src/distributed.rs @@ -16,15 +16,15 @@ pub use crate::component::Component; use crate::transports::nats::DRTNatsClientPrometheusMetrics; use crate::{ + ErrorContext, RuntimeCallback, component::{self, ComponentBuilder, Endpoint, InstanceSource, Namespace}, discovery::DiscoveryClient, metrics::MetricsRegistry, service::ServiceClient, transports::{etcd, nats, tcp}, - ErrorContext, RuntimeCallback, }; -use super::{error, Arc, DistributedRuntime, OnceCell, Result, Runtime, SystemHealth, Weak, OK}; +use super::{Arc, DistributedRuntime, OK, OnceCell, Result, Runtime, SystemHealth, Weak, error}; use std::sync::OnceLock; use derive_getters::Dissolve; @@ -164,7 +164,9 @@ impl DistributedRuntime { tracing::warn!("Failed to initialize system status start time: {}", e); } - tracing::debug!("System status server HTTP endpoints disabled, but uptime metrics are being tracked"); + tracing::debug!( + "System status server HTTP endpoints disabled, but uptime metrics are being tracked" + ); } Ok(distributed_runtime) diff --git a/lib/runtime/src/instances.rs b/lib/runtime/src/instances.rs index d436a0bd38..0cc88b3a0c 100644 --- a/lib/runtime/src/instances.rs +++ b/lib/runtime/src/instances.rs @@ -7,7 +7,7 @@ //! the entire distributed system, complementing the component-specific //! instance listing in `component.rs`. -use crate::component::{Instance, INSTANCE_ROOT_PATH}; +use crate::component::{INSTANCE_ROOT_PATH, Instance}; use crate::transports::etcd::Client as EtcdClient; pub async fn list_all_instances(etcd_client: &EtcdClient) -> anyhow::Result> { diff --git a/lib/runtime/src/lib.rs b/lib/runtime/src/lib.rs index b8c443163a..5dc01ee7e1 100644 --- a/lib/runtime/src/lib.rs +++ b/lib/runtime/src/lib.rs @@ -25,7 +25,7 @@ use std::{ use tokio::sync::Mutex; pub use anyhow::{ - anyhow as error, bail as raise, Context as ErrorContext, Error, Ok as OK, Result, + Context as ErrorContext, Error, Ok as OK, Result, anyhow as error, bail as raise, }; use async_once_cell::OnceCell; diff --git a/lib/runtime/src/logging.rs b/lib/runtime/src/logging.rs index 3b08b40db5..444d1c42a6 100644 --- a/lib/runtime/src/logging.rs +++ b/lib/runtime/src/logging.rs @@ -30,43 +30,43 @@ use std::collections::{BTreeMap, HashMap}; use std::sync::Once; use figment::{ - providers::{Format, Serialized, Toml}, Figment, + providers::{Format, Serialized, Toml}, }; use serde::{Deserialize, Serialize}; use tracing::level_filters::LevelFilter; use tracing::{Event, Subscriber}; +use tracing_subscriber::EnvFilter; use tracing_subscriber::fmt::time::FormatTime; use tracing_subscriber::fmt::time::LocalTime; use tracing_subscriber::fmt::time::SystemTime; use tracing_subscriber::fmt::time::UtcTime; -use tracing_subscriber::fmt::{format::Writer, FormattedFields}; use tracing_subscriber::fmt::{FmtContext, FormatFields}; +use tracing_subscriber::fmt::{FormattedFields, format::Writer}; use tracing_subscriber::prelude::*; use tracing_subscriber::registry::LookupSpan; -use tracing_subscriber::EnvFilter; use tracing_subscriber::{filter::Directive, fmt}; use crate::config::{disable_ansi_logging, jsonl_logging_enabled}; use async_nats::{HeaderMap, HeaderValue}; use axum::extract::FromRequestParts; use axum::http; -use axum::http::request::Parts; use axum::http::Request; +use axum::http::request::Parts; use serde_json::Value; use std::convert::Infallible; use std::time::Instant; use tower_http::trace::{DefaultMakeSpan, TraceLayer}; -use tracing::field::Field; -use tracing::span; use tracing::Id; use tracing::Span; +use tracing::field::Field; +use tracing::span; +use tracing_subscriber::Layer; +use tracing_subscriber::Registry; use tracing_subscriber::field::Visit; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::Context; use tracing_subscriber::registry::SpanData; -use tracing_subscriber::Layer; -use tracing_subscriber::Registry; use uuid::Uuid; /// ENV used to set the log level @@ -340,18 +340,15 @@ where x_dynamo_request_id = Some(x_request_id_input.to_string()); } - if parent_id.is_none() { - if let Some(parent_span_id) = ctx.current_span().id() { - if let Some(parent_span) = ctx.span(parent_span_id) { - let parent_ext = parent_span.extensions(); - if let Some(parent_tracing_context) = - parent_ext.get::() - { - trace_id = Some(parent_tracing_context.trace_id.clone()); - parent_id = Some(parent_tracing_context.span_id.clone()); - tracestate = parent_tracing_context.tracestate.clone(); - } - } + if parent_id.is_none() + && let Some(parent_span_id) = ctx.current_span().id() + && let Some(parent_span) = ctx.span(parent_span_id) + { + let parent_ext = parent_span.extensions(); + if let Some(parent_tracing_context) = parent_ext.get::() { + trace_id = Some(parent_tracing_context.trace_id.clone()); + parent_id = Some(parent_tracing_context.span_id.clone()); + tracestate = parent_tracing_context.tracestate.clone(); } } @@ -787,7 +784,7 @@ impl tracing::field::Visit for JsonVisitor { #[cfg(test)] pub mod tests { use super::*; - use anyhow::{anyhow, Result}; + use anyhow::{Result, anyhow}; use chrono::{DateTime, Utc}; use jsonschema::{Draft, JSONSchema}; use serde_json::Value; @@ -1009,40 +1006,37 @@ pub mod tests { // Parent span has no parent_id for log_line in &lines { - if let Some(span_name) = log_line.get("span_name") { - if let Some(span_name_str) = span_name.as_str() { - if span_name_str == "parent" { - assert!(log_line.get("parent_id").is_none()); - } - } + if let Some(span_name) = log_line.get("span_name") + && let Some(span_name_str) = span_name.as_str() + && span_name_str == "parent" + { + assert!(log_line.get("parent_id").is_none()); } } // Child span's parent_id is parent_span_id for log_line in &lines { - if let Some(span_name) = log_line.get("span_name") { - if let Some(span_name_str) = span_name.as_str() { - if span_name_str == "child" { - assert_eq!( - log_line.get("parent_id").unwrap().as_str().unwrap(), - &parent_span_id - ); - } - } + if let Some(span_name) = log_line.get("span_name") + && let Some(span_name_str) = span_name.as_str() + && span_name_str == "child" + { + assert_eq!( + log_line.get("parent_id").unwrap().as_str().unwrap(), + &parent_span_id + ); } } // Grandchild span's parent_id is child_span_id for log_line in &lines { - if let Some(span_name) = log_line.get("span_name") { - if let Some(span_name_str) = span_name.as_str() { - if span_name_str == "grandchild" { - assert_eq!( - log_line.get("parent_id").unwrap().as_str().unwrap(), - &child_span_id - ); - } - } + if let Some(span_name) = log_line.get("span_name") + && let Some(span_name_str) = span_name.as_str() + && span_name_str == "grandchild" + { + assert_eq!( + log_line.get("parent_id").unwrap().as_str().unwrap(), + &child_span_id + ); } } diff --git a/lib/runtime/src/metrics.rs b/lib/runtime/src/metrics.rs index cd01f6a244..46882034ea 100644 --- a/lib/runtime/src/metrics.rs +++ b/lib/runtime/src/metrics.rs @@ -33,14 +33,14 @@ use std::collections::HashMap; // Import commonly used items to avoid verbose prefixes use prometheus_names::{ - build_metric_name, labels, name_prefix, nats_client, nats_service, work_handler, - COMPONENT_NATS_METRICS, DRT_NATS_METRICS, + COMPONENT_NATS_METRICS, DRT_NATS_METRICS, build_metric_name, labels, name_prefix, nats_client, + nats_service, work_handler, }; // Pipeline imports for endpoint creation use crate::pipeline::{ - async_trait, network::Ingress, AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, - ResponseStream, SingleIn, + AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, ResponseStream, SingleIn, async_trait, + network::Ingress, }; use crate::protocols::annotated::Annotated; use crate::stream; @@ -985,9 +985,11 @@ mod test_metricsregistry_prefixes { // Valid namespace works let valid_namespace = drt.namespace("ns567").unwrap(); - assert!(valid_namespace - .create_counter("test_counter", "A test counter", &[]) - .is_ok()); + assert!( + valid_namespace + .create_counter("test_counter", "A test counter", &[]) + .is_ok() + ); } #[tokio::test] @@ -1042,8 +1044,8 @@ mod test_metricsregistry_prefixes { #[cfg(test)] mod test_metricsregistry_prometheus_fmt_outputs { use super::prometheus_names::name_prefix; - use super::prometheus_names::{nats_client, nats_service}; use super::prometheus_names::{COMPONENT_NATS_METRICS, DRT_NATS_METRICS}; + use super::prometheus_names::{nats_client, nats_service}; use super::*; use crate::distributed::test_helpers::create_test_drt_async; use prometheus::Counter; @@ -1289,9 +1291,11 @@ dynamo_component_nats_service_total_errors 5"#; // Test extract_metrics (only actual metric lines, excluding help/type) let metrics_only = super::test_helpers::extract_metrics(test_input); assert_eq!(metrics_only.len(), 6); // 6 actual metric lines (excluding help/type) - assert!(metrics_only - .iter() - .all(|line| line.starts_with("dynamo_component") && !line.starts_with("#"))); + assert!( + metrics_only + .iter() + .all(|line| line.starts_with("dynamo_component") && !line.starts_with("#")) + ); println!("✓ All refactored filter functions work correctly!"); } @@ -1301,13 +1305,13 @@ dynamo_component_nats_service_total_errors 5"#; #[cfg(test)] mod test_metricsregistry_nats { use super::prometheus_names::name_prefix; - use super::prometheus_names::{nats_client, nats_service}; use super::prometheus_names::{COMPONENT_NATS_METRICS, DRT_NATS_METRICS}; + use super::prometheus_names::{nats_client, nats_service}; use super::*; use crate::distributed::test_helpers::create_test_drt_async; use crate::pipeline::PushRouter; use crate::{DistributedRuntime, Runtime}; - use tokio::time::{sleep, Duration}; + use tokio::time::{Duration, sleep}; #[tokio::test] async fn test_drt_nats_metrics() { // Setup real DRT and registry using the test-friendly constructor @@ -1361,8 +1365,7 @@ mod test_metricsregistry_nats { // Compare the sorted lists assert_eq!( - actual_drt_nats_metrics_sorted, - expect_drt_nats_metrics_sorted, + actual_drt_nats_metrics_sorted, expect_drt_nats_metrics_sorted, "DRT_NATS_METRICS with prefix and expected_nats_metrics should be identical when sorted" ); @@ -1429,8 +1432,7 @@ mod test_metricsregistry_nats { // Compare the sorted lists assert_eq!( - actual_component_nats_metrics_sorted, - expect_component_nats_metrics_sorted, + actual_component_nats_metrics_sorted, expect_component_nats_metrics_sorted, "COMPONENT_NATS_METRICS with prefix and expected_nats_metrics should be identical when sorted" ); diff --git a/lib/runtime/src/pipeline.rs b/lib/runtime/src/pipeline.rs index 84338f39a1..3ff38cdb32 100644 --- a/lib/runtime/src/pipeline.rs +++ b/lib/runtime/src/pipeline.rs @@ -31,8 +31,8 @@ pub use network::egress::push_router::{PushRouter, RouterMode}; pub mod registry; pub use crate::engine::{ - self as engine, async_trait, AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, Data, - DataStream, Engine, EngineStream, EngineUnary, ResponseStream, + self as engine, AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, Data, DataStream, + Engine, EngineStream, EngineUnary, ResponseStream, async_trait, }; pub use anyhow::Error; pub use context::Context; diff --git a/lib/runtime/src/pipeline/context.rs b/lib/runtime/src/pipeline/context.rs index 94f41b3b49..7833fc5c4b 100644 --- a/lib/runtime/src/pipeline/context.rs +++ b/lib/runtime/src/pipeline/context.rs @@ -316,7 +316,7 @@ impl From> for StreamContext { // TODO - refactor here - this came from the dynamo.llm-async-engine crate -use tokio::sync::watch::{channel, Receiver, Sender}; +use tokio::sync::watch::{Receiver, Sender, channel}; #[derive(Debug, Eq, PartialEq)] enum State { diff --git a/lib/runtime/src/pipeline/error.rs b/lib/runtime/src/pipeline/error.rs index 794cf2228b..affcc4f06c 100644 --- a/lib/runtime/src/pipeline/error.rs +++ b/lib/runtime/src/pipeline/error.rs @@ -17,7 +17,7 @@ // use async_nats::error::Error as NatsError; -pub use anyhow::{anyhow, anyhow as error, bail, ensure, Context, Error, Result}; +pub use anyhow::{Context, Error, Result, anyhow, anyhow as error, bail, ensure}; pub trait PipelineErrorExt { /// Downcast the [`Error`] to a [`PipelineError`] diff --git a/lib/runtime/src/pipeline/network.rs b/lib/runtime/src/pipeline/network.rs index 16154f3343..00adaa153a 100644 --- a/lib/runtime/src/pipeline/network.rs +++ b/lib/runtime/src/pipeline/network.rs @@ -33,8 +33,8 @@ use super::{AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, Respons use serde::{Deserialize, Serialize}; use super::{ - context, AsyncTransportEngine, Context, Data, Error, ManyOut, PipelineError, PipelineIO, - SegmentSource, ServiceBackend, ServiceEngine, SingleIn, Source, + AsyncTransportEngine, Context, Data, Error, ManyOut, PipelineError, PipelineIO, SegmentSource, + ServiceBackend, ServiceEngine, SingleIn, Source, context, }; use ingress::push_handler::WorkHandlerMetrics; diff --git a/lib/runtime/src/pipeline/network/codec/two_part.rs b/lib/runtime/src/pipeline/network/codec/two_part.rs index c6bf5dc34f..a53eb34644 100644 --- a/lib/runtime/src/pipeline/network/codec/two_part.rs +++ b/lib/runtime/src/pipeline/network/codec/two_part.rs @@ -70,10 +70,10 @@ impl Decoder for TwoPartCodec { let total_len = 24 + header_len + body_len; // Check if total_len exceeds max_message_size - if let Some(max_size) = self.max_message_size { - if total_len > max_size { - return Err(TwoPartCodecError::MessageTooLarge(total_len, max_size)); - } + if let Some(max_size) = self.max_message_size + && total_len > max_size + { + return Err(TwoPartCodecError::MessageTooLarge(total_len, max_size)); } // Check if enough data is available @@ -124,10 +124,10 @@ impl Encoder for TwoPartCodec { let total_len = 24 + header_len + body_len; // 24 bytes for lengths and checksum // Check if total_len exceeds max_message_size - if let Some(max_size) = self.max_message_size { - if total_len > max_size { - return Err(TwoPartCodecError::MessageTooLarge(total_len, max_size)); - } + if let Some(max_size) = self.max_message_size + && total_len > max_size + { + return Err(TwoPartCodecError::MessageTooLarge(total_len, max_size)); } dst.put_u64(header_len as u64); diff --git a/lib/runtime/src/pipeline/network/egress/addressed_router.rs b/lib/runtime/src/pipeline/network/egress/addressed_router.rs index 3400c0a4b7..22fc5437d8 100644 --- a/lib/runtime/src/pipeline/network/egress/addressed_router.rs +++ b/lib/runtime/src/pipeline/network/egress/addressed_router.rs @@ -18,10 +18,10 @@ use async_nats::{HeaderMap, HeaderValue}; use tracing as log; use super::*; -use crate::logging::get_distributed_tracing_context; use crate::logging::DistributedTraceContext; -use crate::{protocols::maybe_error::MaybeError, Result}; -use tokio_stream::{wrappers::ReceiverStream, StreamExt, StreamNotifyClose}; +use crate::logging::get_distributed_tracing_context; +use crate::{Result, protocols::maybe_error::MaybeError}; +use tokio_stream::{StreamExt, StreamNotifyClose, wrappers::ReceiverStream}; use tracing::Instrument; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/lib/runtime/src/pipeline/network/egress/push_router.rs b/lib/runtime/src/pipeline/network/egress/push_router.rs index 4700f838f9..6e7b858399 100644 --- a/lib/runtime/src/pipeline/network/egress/push_router.rs +++ b/lib/runtime/src/pipeline/network/egress/push_router.rs @@ -7,8 +7,8 @@ use crate::{ component::{Client, Endpoint, InstanceSource}, engine::{AsyncEngine, Data}, pipeline::{ - error::{PipelineError, PipelineErrorExt}, AddressedPushRouter, AddressedRequest, Error, ManyOut, SingleIn, + error::{PipelineError, PipelineErrorExt}, }, protocols::maybe_error::MaybeError, traits::DistributedRuntimeProvider, @@ -23,8 +23,8 @@ use std::{ future::Future, marker::PhantomData, sync::{ - atomic::{AtomicU64, Ordering}, Arc, + atomic::{AtomicU64, Ordering}, }, }; use tokio_stream::StreamExt; @@ -242,10 +242,10 @@ where Ok(ResponseStream::new(Box::pin(stream), engine_ctx)) } Err(err) => { - if let Some(req_err) = err.downcast_ref::() { - if matches!(req_err.kind(), NatsNoResponders) { - self.client.report_instance_down(instance_id); - } + if let Some(req_err) = err.downcast_ref::() + && matches!(req_err.kind(), NatsNoResponders) + { + self.client.report_instance_down(instance_id); } Err(err) } diff --git a/lib/runtime/src/pipeline/network/ingress/push_endpoint.rs b/lib/runtime/src/pipeline/network/ingress/push_endpoint.rs index a407bc72b0..f2edcf5770 100644 --- a/lib/runtime/src/pipeline/network/ingress/push_endpoint.rs +++ b/lib/runtime/src/pipeline/network/ingress/push_endpoint.rs @@ -4,10 +4,10 @@ use std::sync::atomic::{AtomicU64, Ordering}; use super::*; +use crate::SystemHealth; use crate::config::HealthStatus; use crate::logging::TraceParent; use crate::protocols::LeaseId; -use crate::SystemHealth; use anyhow::Result; use async_nats::service::endpoint::Endpoint; use derive_builder::Builder; @@ -77,7 +77,10 @@ impl PushEndpoint { if let Some(req) = req { let response = "".to_string(); if let Err(e) = req.respond(Ok(response.into())).await { - tracing::warn!("Failed to respond to request; this may indicate the request has shutdown: {:?}", e); + tracing::warn!( + "Failed to respond to request; this may indicate the request has shutdown: {:?}", + e + ); } let ingress = self.service_handler.clone(); diff --git a/lib/runtime/src/pipeline/network/ingress/push_handler.rs b/lib/runtime/src/pipeline/network/ingress/push_handler.rs index f2d3bb6edb..d79c868adb 100644 --- a/lib/runtime/src/pipeline/network/ingress/push_handler.rs +++ b/lib/runtime/src/pipeline/network/ingress/push_handler.rs @@ -19,8 +19,8 @@ use prometheus::{Histogram, IntCounter, IntCounterVec, IntGauge}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::time::Instant; -use tracing::info_span; use tracing::Instrument; +use tracing::info_span; /// Metrics configuration for profiling work handlers #[derive(Clone, Debug)] @@ -175,9 +175,9 @@ where .with_label_values(&["deserialization"]) .inc(); } - return Err(PipelineError::DeserializationError( - format!("Failed deserializing to RequestControlMessage. err={err}, json_str={json_str}"), - )); + return Err(PipelineError::DeserializationError(format!( + "Failed deserializing to RequestControlMessage. err={err}, json_str={json_str}" + ))); } }; let request: T = serde_json::from_slice(&data)?; @@ -189,7 +189,9 @@ where .with_label_values(&["invalid_message"]) .inc(); } - return Err(PipelineError::Generic(String::from("Unexpected message from work queue; unable extract a TwoPartMessage with a header and data"))); + return Err(PipelineError::Generic(String::from( + "Unexpected message from work queue; unable extract a TwoPartMessage with a header and data", + ))); } }; diff --git a/lib/runtime/src/pipeline/network/tcp.rs b/lib/runtime/src/pipeline/network/tcp.rs index dc1ac242a2..dc04d1df39 100644 --- a/lib/runtime/src/pipeline/network/tcp.rs +++ b/lib/runtime/src/pipeline/network/tcp.rs @@ -37,8 +37,8 @@ use serde::{Deserialize, Serialize}; #[allow(unused_imports)] use super::{ - codec::TwoPartCodec, ConnectionInfo, PendingConnections, RegisteredStream, ResponseService, - StreamOptions, StreamReceiver, StreamSender, StreamType, + ConnectionInfo, PendingConnections, RegisteredStream, ResponseService, StreamOptions, + StreamReceiver, StreamSender, StreamType, codec::TwoPartCodec, }; const TCP_TRANSPORT: &str = "tcp_server"; diff --git a/lib/runtime/src/pipeline/network/tcp/client.rs b/lib/runtime/src/pipeline/network/tcp/client.rs index f7030066fb..13e96f7ec4 100644 --- a/lib/runtime/src/pipeline/network/tcp/client.rs +++ b/lib/runtime/src/pipeline/network/tcp/client.rs @@ -27,11 +27,11 @@ use tokio_util::codec::{FramedRead, FramedWrite}; use super::{CallHomeHandshake, ControlMessage, TcpStreamConnectionInfo}; use crate::engine::AsyncEngineContext; use crate::pipeline::network::{ + ConnectionInfo, ResponseStreamPrologue, StreamSender, codec::{TwoPartCodec, TwoPartMessage}, tcp::StreamType, - ConnectionInfo, ResponseStreamPrologue, StreamSender, }; -use crate::{error, ErrorContext, Result}; // Import SinkExt to use the `send` method +use crate::{ErrorContext, Result, error}; // Import SinkExt to use the `send` method #[allow(dead_code)] pub struct TcpClient { diff --git a/lib/runtime/src/pipeline/network/tcp/server.rs b/lib/runtime/src/pipeline/network/tcp/server.rs index 102b10bd19..cfa9015e4c 100644 --- a/lib/runtime/src/pipeline/network/tcp/server.rs +++ b/lib/runtime/src/pipeline/network/tcp/server.rs @@ -26,7 +26,7 @@ use tokio::sync::Mutex; use bytes::Bytes; use derive_builder::Builder; use futures::{SinkExt, StreamExt}; -use local_ip_address::{list_afinet_netifas, local_ip, local_ipv6, Error}; +use local_ip_address::{Error, list_afinet_netifas, local_ip, local_ipv6}; use serde::{Deserialize, Serialize}; use tokio::{ io::AsyncWriteExt, @@ -41,14 +41,14 @@ use super::{ }; use crate::engine::AsyncEngineContext; use crate::pipeline::{ + PipelineError, network::{ + ResponseService, ResponseStreamPrologue, codec::{TwoPartMessage, TwoPartMessageType}, tcp::StreamType, - ResponseService, ResponseStreamPrologue, }, - PipelineError, }; -use crate::{error, ErrorContext, Result}; +use crate::{ErrorContext, Result, error}; #[allow(dead_code)] type ResponseType = TwoPartMessage; @@ -461,7 +461,9 @@ async fn tcp_listener( })) .is_err() { - return Err(error!("The requester of the stream has been dropped before the connection was established")); + return Err(error!( + "The requester of the stream has been dropped before the connection was established" + )); } let (control_tx, control_rx) = mpsc::channel::(1); @@ -539,13 +541,12 @@ async fn tcp_listener( } } - if !data.is_empty() { - if let Err(err) = response_tx.send(data).await { + if !data.is_empty() + && let Err(err) = response_tx.send(data).await { tracing::debug!("forwarding body/data message to response channel failed: {}", err); control_tx.send(ControlMessage::Kill).await.expect("the control channel should not be closed"); break; }; - } } Some(Err(_)) => { // TODO(#171) - address fatal errors diff --git a/lib/runtime/src/pipeline/nodes/sinks.rs b/lib/runtime/src/pipeline/nodes/sinks.rs index c8ea4e4418..0bb7822641 100644 --- a/lib/runtime/src/pipeline/nodes/sinks.rs +++ b/lib/runtime/src/pipeline/nodes/sinks.rs @@ -14,7 +14,7 @@ // limitations under the License. use super::{ - async_trait, private::Token, Arc, Edge, OnceLock, PipelineError, Service, Sink, Source, + Arc, Edge, OnceLock, PipelineError, Service, Sink, Source, async_trait, private::Token, }; use crate::pipeline::{PipelineIO, ServiceEngine}; diff --git a/lib/runtime/src/pipeline/nodes/sources/base.rs b/lib/runtime/src/pipeline/nodes/sources/base.rs index 9cf7cba33f..b9dd2f60a4 100644 --- a/lib/runtime/src/pipeline/nodes/sources/base.rs +++ b/lib/runtime/src/pipeline/nodes/sources/base.rs @@ -83,7 +83,7 @@ impl AsyncEngine for Fro #[cfg(test)] mod tests { use super::*; - use crate::pipeline::{error::PipelineErrorExt, ManyOut, SingleIn}; + use crate::pipeline::{ManyOut, SingleIn, error::PipelineErrorExt}; #[tokio::test] async fn test_frontend_no_edge() { diff --git a/lib/runtime/src/protocols/annotated.rs b/lib/runtime/src/protocols/annotated.rs index 33ffa976f9..bf43d1fc79 100644 --- a/lib/runtime/src/protocols/annotated.rs +++ b/lib/runtime/src/protocols/annotated.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use super::*; -use crate::{error, Result}; +use crate::{Result, error}; use maybe_error::MaybeError; pub trait AnnotationsProvider { @@ -68,13 +68,13 @@ impl Annotated { /// Convert to a [`Result`] /// If [`Self::event`] is "error", return an error message(s) held by [`Self::comment`] pub fn ok(self) -> Result { - if let Some(event) = &self.event { - if event == "error" { - return Err(self - .comment - .unwrap_or(vec!["unknown error".to_string()]) - .join(", ")); - } + if let Some(event) = &self.event + && event == "error" + { + return Err(self + .comment + .unwrap_or(vec!["unknown error".to_string()]) + .join(", ")); } Ok(self) } @@ -125,10 +125,11 @@ impl Annotated { match self.data { Some(data) => Ok(Some(data)), None => match self.event { - Some(event) if event == "error" => Err(error!(self - .comment - .unwrap_or(vec!["unknown error".to_string()]) - .join(", ")))?, + Some(event) if event == "error" => Err(error!( + self.comment + .unwrap_or(vec!["unknown error".to_string()]) + .join(", ") + ))?, _ => Ok(None), }, } @@ -145,10 +146,10 @@ where fn err(&self) -> Option { if self.is_error() { - if let Some(comment) = &self.comment { - if !comment.is_empty() { - return Some(anyhow::Error::msg(comment.join("; "))); - } + if let Some(comment) = &self.comment + && !comment.is_empty() + { + return Some(anyhow::Error::msg(comment.join("; "))); } Some(anyhow::Error::msg("unknown error")) } else { diff --git a/lib/runtime/src/runtime.rs b/lib/runtime/src/runtime.rs index 0a5b87013a..700e92b27d 100644 --- a/lib/runtime/src/runtime.rs +++ b/lib/runtime/src/runtime.rs @@ -25,7 +25,7 @@ //! Notes: We will need to do an evaluation on what is fully public, what is pub(crate) and what is //! private; however, for now we are exposing most objects as fully public while the API is maturing. -use super::{error, Result, Runtime, RuntimeType}; +use super::{Result, Runtime, RuntimeType, error}; use crate::config::{self, RuntimeConfig}; use futures::Future; diff --git a/lib/runtime/src/service.rs b/lib/runtime/src/service.rs index 7fcb6bf83a..1ea0ded177 100644 --- a/lib/runtime/src/service.rs +++ b/lib/runtime/src/service.rs @@ -20,13 +20,13 @@ // component's "service state" use crate::{ + DistributedRuntime, Result, component::Component, error, - metrics::{prometheus_names, prometheus_names::nats_service, MetricsRegistry}, + metrics::{MetricsRegistry, prometheus_names, prometheus_names::nats_service}, traits::*, transports::nats, utils::stream, - DistributedRuntime, Result, }; use async_nats::Message; @@ -35,7 +35,7 @@ use bytes::Bytes; use derive_getters::Dissolve; use futures::stream::{StreamExt, TryStreamExt}; use prometheus; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use std::time::Duration; pub struct ServiceClient { diff --git a/lib/runtime/src/slug.rs b/lib/runtime/src/slug.rs index e15eec6607..30349c5ae3 100644 --- a/lib/runtime/src/slug.rs +++ b/lib/runtime/src/slug.rs @@ -43,11 +43,7 @@ impl Slug { .chars() .map(|c| { let is_valid = c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_'; - if is_valid { - c - } else { - REPLACEMENT_CHAR - } + if is_valid { c } else { REPLACEMENT_CHAR } }) .collect::(); Slug::new(out) @@ -61,11 +57,7 @@ impl Slug { .chars() .map(|c| { let is_valid = c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'; - if is_valid { - c - } else { - REPLACEMENT_CHAR - } + if is_valid { c } else { REPLACEMENT_CHAR } }) .collect::(); let hash = blake3::hash(s.as_bytes()).to_string(); diff --git a/lib/runtime/src/storage/key_value_store.rs b/lib/runtime/src/storage/key_value_store.rs index 1a394b5d3a..86f0692aa3 100644 --- a/lib/runtime/src/storage/key_value_store.rs +++ b/lib/runtime/src/storage/key_value_store.rs @@ -22,8 +22,8 @@ use std::pin::Pin; use std::sync::Arc; use std::time::Duration; -use crate::slug::Slug; use crate::CancellationToken; +use crate::slug::Slug; use async_trait::async_trait; use futures::StreamExt; use serde::{Deserialize, Serialize}; @@ -278,7 +278,7 @@ mod tests { use std::sync::Arc; use super::*; - use futures::{pin_mut, StreamExt}; + use futures::{StreamExt, pin_mut}; const BUCKET_NAME: &str = "mdc"; diff --git a/lib/runtime/src/storage/key_value_store/etcd.rs b/lib/runtime/src/storage/key_value_store/etcd.rs index e6c71a261a..cde38aab76 100644 --- a/lib/runtime/src/storage/key_value_store/etcd.rs +++ b/lib/runtime/src/storage/key_value_store/etcd.rs @@ -186,11 +186,10 @@ impl EtcdBucket { // Key already existed, get its version if let Some(etcd_client::TxnOpResponse::Get(get_resp)) = result.op_responses().into_iter().next() + && let Some(kv) = get_resp.kvs().first() { - if let Some(kv) = get_resp.kvs().first() { - let version = kv.version() as u64; - return Ok(StorageOutcome::Exists(version)); - } + let version = kv.version() as u64; + return Ok(StorageOutcome::Exists(version)); } // Shouldn't happen, but handle edge case Err(StorageError::EtcdError( @@ -259,7 +258,7 @@ fn make_key(bucket_name: &str, key: &str) -> String { #[cfg(test)] mod concurrent_create_tests { use super::*; - use crate::{distributed::DistributedConfig, DistributedRuntime, Runtime}; + use crate::{DistributedRuntime, Runtime, distributed::DistributedConfig}; use std::sync::Arc; use tokio::sync::Barrier; diff --git a/lib/runtime/src/storage/key_value_store/mem.rs b/lib/runtime/src/storage/key_value_store/mem.rs index dd4853a01d..371a672a1a 100644 --- a/lib/runtime/src/storage/key_value_store/mem.rs +++ b/lib/runtime/src/storage/key_value_store/mem.rs @@ -20,8 +20,8 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::sync::Mutex; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use super::{KeyValueBucket, KeyValueStore, StorageError, StorageOutcome}; diff --git a/lib/runtime/src/system_status_server.rs b/lib/runtime/src/system_status_server.rs index 3a71daa933..44ef651ec2 100644 --- a/lib/runtime/src/system_status_server.rs +++ b/lib/runtime/src/system_status_server.rs @@ -17,7 +17,7 @@ use crate::config::HealthStatus; use crate::logging::make_request_span; use crate::metrics::MetricsRegistry; use crate::traits::DistributedRuntimeProvider; -use axum::{http::StatusCode, response::IntoResponse, routing::get, Router}; +use axum::{Router, http::StatusCode, response::IntoResponse, routing::get}; use serde_json::json; use std::sync::{Arc, OnceLock}; use std::time::Instant; diff --git a/lib/runtime/src/transports/etcd.rs b/lib/runtime/src/transports/etcd.rs index e204831806..5352219aa7 100644 --- a/lib/runtime/src/transports/etcd.rs +++ b/lib/runtime/src/transports/etcd.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{error, CancellationToken, ErrorContext, Result, Runtime}; +use crate::{CancellationToken, ErrorContext, Result, Runtime, error}; use async_nats::jetstream::kv; use derive_builder::Builder; @@ -21,7 +21,7 @@ use derive_getters::Dissolve; use futures::StreamExt; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::{mpsc, RwLock}; +use tokio::sync::{RwLock, mpsc}; use validator::Validate; use etcd_client::{ @@ -29,7 +29,7 @@ use etcd_client::{ TlsOptions, Txn, TxnOp, TxnOpResponse, WatchOptions, Watcher, }; pub use etcd_client::{ConnectOptions, KeyValue, LeaseClient}; -use tokio::time::{interval, Duration}; +use tokio::time::{Duration, interval}; mod lease; mod path; @@ -605,7 +605,7 @@ impl KvCache { #[cfg(feature = "integration")] #[cfg(test)] mod tests { - use crate::{distributed::DistributedConfig, DistributedRuntime}; + use crate::{DistributedRuntime, distributed::DistributedConfig}; use super::*; diff --git a/lib/runtime/src/transports/nats.rs b/lib/runtime/src/transports/nats.rs index ee70f2a17f..0313facc70 100644 --- a/lib/runtime/src/transports/nats.rs +++ b/lib/runtime/src/transports/nats.rs @@ -28,10 +28,10 @@ //! - `NATS_AUTH_CREDENTIALS_FILE`: the path to the credentials file //! //! Note: `NATS_AUTH_USERNAME` and `NATS_AUTH_PASSWORD` must be used together. -use crate::{metrics::MetricsRegistry, Result}; +use crate::{Result, metrics::MetricsRegistry}; use async_nats::connection::State; -use async_nats::{client, jetstream, Subscriber}; +use async_nats::{Subscriber, client, jetstream}; use bytes::Bytes; use derive_builder::Builder; use futures::{StreamExt, TryStreamExt}; diff --git a/lib/runtime/src/transports/zmq.rs b/lib/runtime/src/transports/zmq.rs index 7f195a2514..860b34100d 100644 --- a/lib/runtime/src/transports/zmq.rs +++ b/lib/runtime/src/transports/zmq.rs @@ -28,7 +28,7 @@ //! equivalent of a connection pool per upstream service at the cost of needing an extra internal //! routing step per service endpoint. -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use async_zmq::{Context, Dealer, Router, Sink, SinkExt, StreamExt}; use bytes::Bytes; use derive_getters::Dissolve; @@ -36,7 +36,7 @@ use futures::TryStreamExt; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, os::fd::FromRawFd, sync::Arc, time::Duration, vec::IntoIter}; use tokio::{ - sync::{mpsc, Mutex}, + sync::{Mutex, mpsc}, task::{JoinError, JoinHandle}, }; use tokio_util::sync::CancellationToken; diff --git a/lib/runtime/src/utils/leader_worker_barrier.rs b/lib/runtime/src/utils/leader_worker_barrier.rs index e1ca9372dd..15f47cd627 100644 --- a/lib/runtime/src/utils/leader_worker_barrier.rs +++ b/lib/runtime/src/utils/leader_worker_barrier.rs @@ -14,10 +14,10 @@ // limitations under the License. use crate::{ - transports::etcd::{Client, WatchEvent}, DistributedRuntime, + transports::etcd::{Client, WatchEvent}, }; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; diff --git a/lib/runtime/src/utils/pool.rs b/lib/runtime/src/utils/pool.rs index 2f2dccea93..0b5acb4e3c 100644 --- a/lib/runtime/src/utils/pool.rs +++ b/lib/runtime/src/utils/pool.rs @@ -283,7 +283,7 @@ impl Clone for Pool { #[cfg(test)] mod tests { use super::*; - use tokio::time::{timeout, Duration}; + use tokio::time::{Duration, timeout}; // Implement Returnable for u32 just for testing impl Returnable for u32 { diff --git a/lib/runtime/src/utils/stream.rs b/lib/runtime/src/utils/stream.rs index c52b5e433d..eef1e4dfba 100644 --- a/lib/runtime/src/utils/stream.rs +++ b/lib/runtime/src/utils/stream.rs @@ -20,7 +20,7 @@ use std::{ task::{Context, Poll}, }; -use tokio::time::{self, sleep_until, Duration, Instant, Sleep}; +use tokio::time::{self, Duration, Instant, Sleep, sleep_until}; pub struct DeadlineStream { stream: S, diff --git a/lib/runtime/src/utils/tasks/critical.rs b/lib/runtime/src/utils/tasks/critical.rs index a0a73995ec..5d8b236f96 100644 --- a/lib/runtime/src/utils/tasks/critical.rs +++ b/lib/runtime/src/utils/tasks/critical.rs @@ -197,14 +197,14 @@ impl CriticalTaskExecutionHandle { /// Note: Both errors and panics trigger parent cancellation immediately via the monitor task. pub async fn join(mut self) -> Result<()> { self.detached = true; - let result = match self.result_receiver.take().unwrap().await { + + match self.result_receiver.take().unwrap().await { Ok(task_result) => task_result, Err(_) => { // This should rarely happen - means monitor task was dropped/cancelled Err(anyhow::anyhow!("Critical task monitor was cancelled")) } - }; - result + } } /// Detach the task. This allows the task to continue running after the handle is dropped. @@ -224,8 +224,8 @@ impl Drop for CriticalTaskExecutionHandle { #[cfg(test)] mod tests { use super::*; - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; + use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::time::Duration; use tokio::time::timeout; diff --git a/lib/runtime/src/utils/tasks/tracker.rs b/lib/runtime/src/utils/tasks/tracker.rs index f65091f1ca..737f315622 100644 --- a/lib/runtime/src/utils/tasks/tracker.rs +++ b/lib/runtime/src/utils/tasks/tracker.rs @@ -380,8 +380,8 @@ use std::future::Future; use std::pin::Pin; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use crate::metrics::MetricsRegistry; use anyhow::Result; @@ -395,7 +395,7 @@ use tokio::sync::Semaphore; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker as TokioTaskTracker; -use tracing::{debug, error, warn, Instrument}; +use tracing::{Instrument, debug, error, warn}; use uuid::Uuid; /// Error type for task execution results @@ -4713,11 +4713,13 @@ mod tests { // Now, trying to create a child should fail let result = parent_clone.child_tracker(); assert!(result.is_err()); - assert!(result - .err() - .unwrap() - .to_string() - .contains("closed parent tracker")); + assert!( + result + .err() + .unwrap() + .to_string() + .contains("closed parent tracker") + ); } #[rstest] @@ -4740,11 +4742,13 @@ mod tests { // Now, trying to create a child with builder should fail let result = parent_clone.child_tracker_builder().build(); assert!(result.is_err()); - assert!(result - .err() - .unwrap() - .to_string() - .contains("closed parent tracker")); + assert!( + result + .err() + .unwrap() + .to_string() + .contains("closed parent tracker") + ); } #[rstest] @@ -4909,9 +4913,11 @@ mod tests { // Test conversion to anyhow::Error let anyhow_error = anyhow::Error::new(continuation_error); - assert!(anyhow_error - .to_string() - .contains("Task failed with continuation")); + assert!( + anyhow_error + .to_string() + .contains("Task failed with continuation") + ); } #[test] @@ -5046,9 +5052,11 @@ mod tests { let anyhow_error = FailedWithContinuation::into_anyhow(source_error, restartable_task); assert!(anyhow_error.has_continuation()); - assert!(anyhow_error - .to_string() - .contains("Task failed with continuation")); + assert!( + anyhow_error + .to_string() + .contains("Task failed with continuation") + ); assert!(anyhow_error.to_string().contains("Computation failed")); } diff --git a/lib/runtime/src/utils/typed_prefix_watcher.rs b/lib/runtime/src/utils/typed_prefix_watcher.rs index 24802c19bc..5fa3f69d49 100644 --- a/lib/runtime/src/utils/typed_prefix_watcher.rs +++ b/lib/runtime/src/utils/typed_prefix_watcher.rs @@ -6,8 +6,8 @@ //! This module provides reusable patterns for watching etcd prefixes and maintaining //! HashMap-based state that automatically updates based on etcd events. -use crate::transports::etcd::{Client as EtcdClient, WatchEvent}; use crate::Result; +use crate::transports::etcd::{Client as EtcdClient, WatchEvent}; use etcd_client::KeyValue; use serde::de::DeserializeOwned; use std::collections::HashMap; diff --git a/lib/runtime/src/utils/worker_monitor.rs b/lib/runtime/src/utils/worker_monitor.rs index ed3ce34d74..d8f0ebca9c 100644 --- a/lib/runtime/src/utils/worker_monitor.rs +++ b/lib/runtime/src/utils/worker_monitor.rs @@ -6,8 +6,8 @@ // different types of workers to define their own load metrics and busy thresholds. use crate::component::{Client, InstanceSource}; -use crate::traits::events::EventSubscriber; use crate::traits::DistributedRuntimeProvider; +use crate::traits::events::EventSubscriber; use crate::utils::typed_prefix_watcher::{key_extractors, watch_prefix_with_extraction}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; diff --git a/lib/runtime/src/worker.rs b/lib/runtime/src/worker.rs index 6553ce595b..f03f2c1580 100644 --- a/lib/runtime/src/worker.rs +++ b/lib/runtime/src/worker.rs @@ -32,7 +32,7 @@ //! and release builds. In development, the default is [DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_DEBUG] and //! in release, the default is [DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_RELEASE]. -use super::{error, CancellationToken, Result, Runtime, RuntimeConfig}; +use super::{CancellationToken, Result, Runtime, RuntimeConfig, error}; use futures::Future; use once_cell::sync::OnceCell; @@ -198,14 +198,13 @@ impl Worker { })))) .expect("Failed to spawn application task"); - let task = INIT + INIT .get() .expect("Application task not initialized") .lock() .unwrap() .take() - .expect("Application initialized; but another thread is awaiting it; Worker.execute() can only be called once"); - task + .expect("Application initialized; but another thread is awaiting it; Worker.execute() can only be called once") } pub fn from_current() -> Result { diff --git a/lib/runtime/tests/common/engines.rs b/lib/runtime/tests/common/engines.rs index 7082f69c3d..83d509f27c 100644 --- a/lib/runtime/tests/common/engines.rs +++ b/lib/runtime/tests/common/engines.rs @@ -27,8 +27,8 @@ use dynamo_runtime::engine::{ }; use dynamo_runtime::pipeline::{ - context::{Context, StreamContext}, Error, ManyOut, SingleIn, + context::{Context, StreamContext}, }; pub type AsyncFn = dyn Fn(T) -> Pin + Send>> + Send + Sync; diff --git a/lib/runtime/tests/common/mock.rs b/lib/runtime/tests/common/mock.rs index 5ca3121c99..c1d605398b 100644 --- a/lib/runtime/tests/common/mock.rs +++ b/lib/runtime/tests/common/mock.rs @@ -25,8 +25,8 @@ use tokio::sync::mpsc; use dynamo_runtime::engine::{AsyncEngine, AsyncEngineContext, Data, ResponseStream}; use dynamo_runtime::pipeline::{ - context::{Context, StreamContext}, Error, ManyOut, PipelineError, PipelineIO, SegmentSource, SingleIn, + context::{Context, StreamContext}, }; #[allow(dead_code)] diff --git a/lib/runtime/tests/lifecycle.rs b/lib/runtime/tests/lifecycle.rs index 78c17462b3..7e07a2da10 100644 --- a/lib/runtime/tests/lifecycle.rs +++ b/lib/runtime/tests/lifecycle.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use dynamo_runtime::{worker::Worker, Result, Runtime}; +use dynamo_runtime::{Result, Runtime, worker::Worker}; async fn hello_world(_runtime: Runtime) -> Result<()> { Ok(()) diff --git a/lib/runtime/tests/pipeline.rs b/lib/runtime/tests/pipeline.rs index 5d0b98b8ea..889ac89e45 100644 --- a/lib/runtime/tests/pipeline.rs +++ b/lib/runtime/tests/pipeline.rs @@ -15,17 +15,17 @@ #![allow(dead_code)] -use futures::{stream, StreamExt}; +use futures::{StreamExt, stream}; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; use dynamo_runtime::engine::ResponseStream; use dynamo_runtime::{ + Error, pipeline::{ - async_trait, AsyncEngine, Data, Event, ManyOut, Operator, ServiceBackend, ServiceEngine, - ServiceFrontend, SingleIn, *, + AsyncEngine, Data, Event, ManyOut, Operator, ServiceBackend, ServiceEngine, + ServiceFrontend, SingleIn, async_trait, *, }, - Error, }; mod common; @@ -153,8 +153,8 @@ fn make_postprocessor() -> Arc>, ManyOut< // Node 0: // [frontend] -------[pre processor]-----> [backend] // [frontend] <----- [post processor] ---- [backend] -fn make_service( -) -> Result, ManyOut>>, PipelineError> { +fn make_service() +-> Result, ManyOut>>, PipelineError> { // Frontend - Callable interface let frontend = ServiceFrontend::, ManyOut>>::new(); @@ -253,14 +253,16 @@ async fn test_disaggregated_service() { // } // assert_eq!(counter, 20); - println!("Test blocked: SegmentSink::attach requires Arc but AsyncEngineStream cannot be Sync"); + println!( + "Test blocked: SegmentSink::attach requires Arc but AsyncEngineStream cannot be Sync" + ); } // Node 0: // [frontend] --> [pre processor] --> [operator] ----------------------> [backend] // [frontend] <---------------------- [operator] <--[post processor] <-- [backend] -fn make_service_with_operator( -) -> Result, ManyOut>>, PipelineError> { +fn make_service_with_operator() +-> Result, ManyOut>>, PipelineError> { // Frontend - Callable interface let frontend = ServiceFrontend::, ManyOut>>::new(); diff --git a/lib/runtime/tests/soak.rs b/lib/runtime/tests/soak.rs index 16fdc896b9..aebabe1b25 100644 --- a/lib/runtime/tests/soak.rs +++ b/lib/runtime/tests/soak.rs @@ -30,18 +30,18 @@ mod integration { pub const DEFAULT_NAMESPACE: &str = "dynamo"; use dynamo_runtime::{ - logging, + DistributedRuntime, ErrorContext, Result, Runtime, Worker, logging, pipeline::{ - async_trait, network::Ingress, AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, - PushRouter, ResponseStream, SingleIn, + AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, PushRouter, ResponseStream, + SingleIn, async_trait, network::Ingress, }, protocols::annotated::Annotated, - stream, DistributedRuntime, ErrorContext, Result, Runtime, Worker, + stream, }; use futures::StreamExt; use std::{ - sync::atomic::{AtomicU64, Ordering}, sync::Arc, + sync::atomic::{AtomicU64, Ordering}, time::Duration, }; use tokio::time::Instant; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b8889a3bb3..b67e7d5348 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.87.0" +channel = "1.89.0" From c9163dc6b373801b8bfb677c4fe5460a3e21198e Mon Sep 17 00:00:00 2001 From: Biswa Panda Date: Fri, 22 Aug 2025 16:47:21 -0700 Subject: [PATCH 12/82] fix: hello world yaml and messages (#2634) Signed-off-by: Jason Zhou --- examples/runtime/hello_world/client.py | 2 +- examples/runtime/hello_world/deploy/hello_world.yaml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/runtime/hello_world/client.py b/examples/runtime/hello_world/client.py index 1eacfcb16e..71643d3988 100644 --- a/examples/runtime/hello_world/client.py +++ b/examples/runtime/hello_world/client.py @@ -40,7 +40,7 @@ async def worker(runtime: DistributedRuntime): try: # Issue request and process the stream idx += 1 - stream = await client.generate(f"Query[{idx}] Hello world") + stream = await client.generate("world,sun,moon,star") async for response in stream: print(response.data()) # Reset backoff on successful iteration diff --git a/examples/runtime/hello_world/deploy/hello_world.yaml b/examples/runtime/hello_world/deploy/hello_world.yaml index c58ac19d5d..1bf010cfc6 100644 --- a/examples/runtime/hello_world/deploy/hello_world.yaml +++ b/examples/runtime/hello_world/deploy/hello_world.yaml @@ -6,6 +6,7 @@ kind: DynamoGraphDeployment metadata: name: hello-world spec: + backendFramework: vllm services: Frontend: livenessProbe: @@ -60,7 +61,7 @@ spec: command: - /bin/sh - -c - - 'grep "Serving endpoint" /tmp/hello_world.log' + - "exit 0" initialDelaySeconds: 60 periodSeconds: 60 timeoutSeconds: 30 @@ -83,4 +84,4 @@ spec: - /bin/sh - -c args: - - python3 hello_world.py 2>&1 | tee /tmp/hello_world.log + - python3 hello_world.py From 2a57fd66b0b8f5ad3c4f6cfb65e386aadd8dca70 Mon Sep 17 00:00:00 2001 From: Keiven C <213854356+keivenchang@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:52:57 -0700 Subject: [PATCH 13/82] fix: move metrics registration to service creation (#2664) Co-authored-by: Keiven Chang Signed-off-by: Jason Zhou --- lib/runtime/src/component.rs | 32 ++----------------------- lib/runtime/src/component/service.rs | 9 +++++++ lib/runtime/src/distributed.rs | 20 ++++------------ lib/runtime/src/metrics.rs | 17 +++++++------ lib/runtime/src/system_status_server.rs | 2 ++ 5 files changed, 28 insertions(+), 52 deletions(-) diff --git a/lib/runtime/src/component.rs b/lib/runtime/src/component.rs index 157d11a279..8ecbe104fe 100644 --- a/lib/runtime/src/component.rs +++ b/lib/runtime/src/component.rs @@ -573,39 +573,11 @@ impl Namespace { /// Create a [`Component`] in the namespace who's endpoints can be discovered with etcd pub fn component(&self, name: impl Into) -> Result { - let component = ComponentBuilder::from_runtime(self.runtime.clone()) + Ok(ComponentBuilder::from_runtime(self.runtime.clone()) .name(name) .namespace(self.clone()) .is_static(self.is_static) - .build()?; - - // Register the metrics callback for this component. - // If registration fails, log a warning but do not propagate the error, - // as metrics are not mission critical and should not block component creation. - if let Err(err) = component.start_scraping_nats_service_component_metrics() { - let error_str = err.to_string(); - - // Check if this is a duplicate metrics registration (expected in some cases) - // or a different error (unexpected) - if error_str.contains("Duplicate metrics") { - // This is not a critical error because it's possible for multiple Components - // with the same service_name to register metrics callbacks. - tracing::debug!( - "Duplicate metrics registration for component '{}' (expected when multiple components share the same service_name): {}", - component.service_name(), - error_str - ); - } else { - // This is unexpected and should be more visible - tracing::warn!( - "Failed to start scraping metrics for component '{}': {}", - component.service_name(), - err - ); - } - } - - Ok(component) + .build()?) } /// Create a [`Namespace`] in the parent namespace diff --git a/lib/runtime/src/component/service.rs b/lib/runtime/src/component/service.rs index 1a74b27f04..98394c08ba 100644 --- a/lib/runtime/src/component/service.rs +++ b/lib/runtime/src/component/service.rs @@ -99,6 +99,15 @@ impl ServiceConfigBuilder { // drop the guard to unlock the mutex drop(guard); + // Register metrics callback. CRITICAL: Never fail service creation for metrics issues. + if let Err(err) = component.start_scraping_nats_service_component_metrics() { + tracing::debug!( + "Metrics registration failed for '{}': {}", + component.service_name(), + err + ); + } + Ok(component) } } diff --git a/lib/runtime/src/distributed.rs b/lib/runtime/src/distributed.rs index e8bd6789f3..15559ec7ea 100644 --- a/lib/runtime/src/distributed.rs +++ b/lib/runtime/src/distributed.rs @@ -272,26 +272,16 @@ impl DistributedRuntime { pub fn add_prometheus_metric( &self, hierarchy: &str, - metric_name: &str, prometheus_metric: Box, ) -> anyhow::Result<()> { let mut registries = self.hierarchy_to_metricsregistry.write().unwrap(); let entry = registries.entry(hierarchy.to_string()).or_default(); - // Try to register the metric and provide better error information - match entry.prometheus_registry.register(prometheus_metric) { - Ok(_) => Ok(()), - Err(e) => { - let error_msg = e.to_string(); - tracing::error!( - hierarchy = ?hierarchy, - error = ?error_msg, - metric_name = ?metric_name, - "Metric registration failed" - ); - Err(e.into()) - } - } + // Try to register the metric + entry + .prometheus_registry + .register(prometheus_metric) + .map_err(|e| e.into()) } /// Add a callback function to metrics registries for the given hierarchies diff --git a/lib/runtime/src/metrics.rs b/lib/runtime/src/metrics.rs index 46882034ea..58a690f854 100644 --- a/lib/runtime/src/metrics.rs +++ b/lib/runtime/src/metrics.rs @@ -392,7 +392,7 @@ fn create_metric( let collector: Box = Box::new(prometheus_metric.clone()); registry .drt() - .add_prometheus_metric(¤t_hierarchy, &metric_name, collector)?; + .add_prometheus_metric(¤t_hierarchy, collector)?; } Ok(prometheus_metric) @@ -1384,6 +1384,9 @@ mod test_metricsregistry_nats { let namespace = drt.namespace("ns789").unwrap(); let components = namespace.component("comp789").unwrap(); + // Create a service to trigger metrics callback registration + let _service = components.service_builder().create().await.unwrap(); + // Get components output which should include NATS client metrics // Additional checks for NATS client metrics (without checking specific values) let component_nats_metrics = @@ -1516,15 +1519,15 @@ mod test_metricsregistry_nats { (build_metric_name(nats_client::CONNECTS), 1.0, 1.0), // Should have 1 connection ( build_metric_name(nats_client::IN_TOTAL_BYTES), - 400.0, - 1500.0, - ), // Wide range around 923 + 800.0, + 4000.0, + ), // Wide range around observed value of 1888 (build_metric_name(nats_client::IN_MESSAGES), 0.0, 5.0), // Wide range around 2 ( build_metric_name(nats_client::OUT_OVERHEAD_BYTES), - 700.0, - 2500.0, - ), // Wide range around 1633 + 1500.0, + 5000.0, + ), // Wide range around observed value of 2752 (build_metric_name(nats_client::OUT_MESSAGES), 0.0, 5.0), // Wide range around 2 // Component NATS metrics (ordered to match COMPONENT_NATS_METRICS) (build_metric_name(nats_service::AVG_PROCESSING_MS), 0.0, 0.0), // No processing yet diff --git a/lib/runtime/src/system_status_server.rs b/lib/runtime/src/system_status_server.rs index 44ef651ec2..b6eca4f785 100644 --- a/lib/runtime/src/system_status_server.rs +++ b/lib/runtime/src/system_status_server.rs @@ -74,6 +74,8 @@ impl SystemStatusState { /// Create new system status server state with the provided metrics registry pub fn new(drt: Arc) -> anyhow::Result { // Note: This metric is created at the DRT level (no namespace), so it will be prefixed with "dynamo_component_" + // TODO(keiven): this is part of another upcoming refactor, where we will no longer + // have this duplicate DRT (and Duplicate metrics error). let uptime_gauge = match drt.as_ref().create_gauge( "uptime_seconds", "Total uptime of the DistributedRuntime in seconds", From f18aee45a9434c11e0758adc562c0e0afa4d4e94 Mon Sep 17 00:00:00 2001 From: Ryan McCormick Date: Fri, 22 Aug 2025 17:01:56 -0700 Subject: [PATCH 14/82] fix: Skip checksum tests in release mode since they're not computed (#2669) Signed-off-by: Jason Zhou --- lib/runtime/src/pipeline/network/codec/two_part.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/runtime/src/pipeline/network/codec/two_part.rs b/lib/runtime/src/pipeline/network/codec/two_part.rs index a53eb34644..e3e76fe624 100644 --- a/lib/runtime/src/pipeline/network/codec/two_part.rs +++ b/lib/runtime/src/pipeline/network/codec/two_part.rs @@ -455,6 +455,8 @@ mod tests { /// Test decoding of a message with checksum mismatch. #[test] + // Checksum only computed in debug mode, so only test in debug mode. + #[cfg(debug_assertions)] fn test_checksum_mismatch() { // Create a message let header_data = Bytes::from("header data"); @@ -646,6 +648,8 @@ mod tests { /// Test handling of corrupted data in a stream #[tokio::test] + // Checksum only computed in debug mode, so only test in debug mode. + #[cfg(debug_assertions)] async fn test_streaming_corrupted_data() { // Create messages let header_data = Bytes::from("header data"); From 6c6679538067ae914a8ee8d93f67a9f12eb7c1e8 Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Sun, 24 Aug 2025 07:37:29 -0600 Subject: [PATCH 15/82] fix: fix manual helm chart (#2648) Signed-off-by: Jason Zhou --- deploy/helm/chart/templates/deployment.yaml | 73 +++++++++++++++++- .../chart/templates/grove-podgangset.yaml | 76 +++++++++++++++++-- deploy/helm/chart/templates/service.yaml | 2 +- 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/deploy/helm/chart/templates/deployment.yaml b/deploy/helm/chart/templates/deployment.yaml index 51579d3254..a835fdfd38 100644 --- a/deploy/helm/chart/templates/deployment.yaml +++ b/deploy/helm/chart/templates/deployment.yaml @@ -39,14 +39,32 @@ spec: containers: - name: {{ $.Release.Name }}-{{ $serviceName | lower }} image: {{ $serviceSpec.extraPodSpec.mainContainer.image }} + {{- if $serviceSpec.extraPodSpec.mainContainer.workingDir }} workingDir: {{ $serviceSpec.extraPodSpec.mainContainer.workingDir }} + {{- end }} {{- if $serviceSpec.extraPodSpec.mainContainer.command }} command: {{- $serviceSpec.extraPodSpec.mainContainer.command | toYaml | nindent 8 }} + {{- else }} + {{- if $serviceSpec.componentType | eq "frontend" }} + command: + - python3 + {{- else }} + command: + - /bin/sh + - -c + {{- if not $serviceSpec.extraPodSpec.mainContainer.args }} + {{- fail (printf "spec.services[%s].extraPodSpec.mainContainer.args must be set for non-frontend components" $serviceName) }} + {{- end }} + {{- end }} {{- end }} {{- if $serviceSpec.extraPodSpec.mainContainer.args }} args: {{- $serviceSpec.extraPodSpec.mainContainer.args | toYaml | nindent 8 }} + {{- else if $serviceSpec.componentType | eq "frontend" }} + args: + - -m + - dynamo.frontend {{- end }} {{ if $serviceSpec.resources }} resources: @@ -77,8 +95,10 @@ spec: name: {{ $serviceSpec.envFromSecret }} {{- end }} env: - - name: DYNAMO_PORT - value: "{{ $.Values.dynamoPort | default 8000 }}" + {{- if $.Values.dynamoNamespace }} + - name: DYN_NAMESPACE + value: {{ $.Values.dynamoNamespace }} + {{- end }} {{- if $.Values.etcdAddr }} - name: ETCD_ENDPOINTS value: "{{ $.Values.etcdAddr }}" @@ -87,9 +107,31 @@ spec: - name: NATS_SERVER value: "{{ $.Values.natsAddr }}" {{- end }} + {{- if $serviceSpec.componentType | eq "frontend" }} + - name: DYNAMO_PORT + value: "{{ $.Values.dynamoPort | default 8000 }}" + - name: DYN_HTTP_PORT + value: "{{ $.Values.dynamoPort | default 8000 }}" + {{- else if $serviceSpec.componentType | eq "worker" }} + - name: DYN_SYSTEM_ENABLED + value: "true" + - name: DYN_SYSTEM_PORT + value: "{{ $.Values.dynamoSystemPort | default 9090 }}" + - name: DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS + value: "[\"generate\"]" + {{- end }} + {{- if $serviceSpec.componentType | eq "frontend" }} + ports: + - name: http + containerPort: {{ $.Values.dynamoPort | default 8000 }} + protocol: TCP + {{- else if $serviceSpec.componentType | eq "worker" }} ports: - - name: health - containerPort: {{ $.Values.healthPort | default 5000 }} + - name: system + containerPort: {{ $.Values.dynamoSystemPort | default 9090 }} + protocol: TCP + {{- end }} + {{- if and $serviceSpec.componentType (or (eq $serviceSpec.componentType "frontend") (eq $serviceSpec.componentType "worker")) }} livenessProbe: {{- if $serviceSpec.livenessProbe }} {{ $serviceSpec.livenessProbe | toYaml | nindent 10 }} @@ -99,11 +141,21 @@ spec: timeoutSeconds: 5 failureThreshold: 10 successThreshold: 1 + {{- if $serviceSpec.componentType | eq "frontend" }} + httpGet: + path: /health + port: http + {{- else if $serviceSpec.componentType | eq "worker" }} + httpGet: + path: /live + port: system + {{- else }} httpGet: path: /healthz port: health scheme: HTTP {{- end }} + {{- end }} readinessProbe: {{- if $serviceSpec.readinessProbe }} {{ $serviceSpec.readinessProbe | toYaml | nindent 10 }} @@ -113,10 +165,23 @@ spec: timeoutSeconds: 5 failureThreshold: 10 successThreshold: 1 + {{- if $serviceSpec.componentType | eq "frontend" }} + exec: + command: + - /bin/sh + - -c + - curl -s http://localhost:${DYNAMO_PORT}/health | jq -e ".status == \"healthy\"" + {{- else if $serviceSpec.componentType | eq "worker" }} + httpGet: + path: /health + port: system + {{- else }} httpGet: path: /readyz port: health scheme: HTTP {{- end }} + {{- end }} + {{- end }} {{- end }} {{- end }} \ No newline at end of file diff --git a/deploy/helm/chart/templates/grove-podgangset.yaml b/deploy/helm/chart/templates/grove-podgangset.yaml index fa5689db1b..5608cea4cb 100644 --- a/deploy/helm/chart/templates/grove-podgangset.yaml +++ b/deploy/helm/chart/templates/grove-podgangset.yaml @@ -23,6 +23,7 @@ metadata: spec: replicas: 1 template: + terminationDelay: 1h cliques: {{- range $serviceName, $serviceSpec := .Values.spec.services }} - name: {{ $serviceName | lower }} @@ -64,14 +65,32 @@ spec: {{- if $serviceSpec.extraPodSpec.mainContainer.command }} command: {{- $serviceSpec.extraPodSpec.mainContainer.command | toYaml | nindent 14 }} + {{- else }} + {{- if $serviceSpec.componentType | eq "frontend" }} + command: + - python3 + {{- else }} + command: + - /bin/sh + - -c + {{- if not $serviceSpec.extraPodSpec.mainContainer.args }} + {{- fail (printf "spec.services[%s].extraPodSpec.mainContainer.args must be set for non-frontend components" $serviceName) }} + {{- end }} + {{- end }} {{- end }} {{- if $serviceSpec.extraPodSpec.mainContainer.args }} args: {{- $serviceSpec.extraPodSpec.mainContainer.args | toYaml | nindent 14 }} + {{- else if $serviceSpec.componentType | eq "frontend" }} + args: + - -m + - dynamo.frontend {{- end }} env: - - name: DYNAMO_PORT - value: "{{ $.Values.dynamoPort | default 8000 }}" + {{- if $.Values.dynamoNamespace }} + - name: DYN_NAMESPACE + value: {{ $.Values.dynamoNamespace }} + {{- end }} {{- if $.Values.etcdAddr }} - name: ETCD_ENDPOINTS value: "{{ $.Values.etcdAddr }}" @@ -80,41 +99,86 @@ spec: - name: NATS_SERVER value: "{{ $.Values.natsAddr }}" {{- end }} + {{- if $serviceSpec.componentType | eq "frontend" }} + - name: DYNAMO_PORT + value: "{{ $.Values.dynamoPort | default 8000 }}" + - name: DYN_HTTP_PORT + value: "{{ $.Values.dynamoPort | default 8000 }}" + {{- else if $serviceSpec.componentType | eq "worker" }} + - name: DYN_SYSTEM_ENABLED + value: "true" + - name: DYN_SYSTEM_PORT + value: "{{ $.Values.dynamoSystemPort | default 9090 }}" + - name: DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS + value: "[\"generate\"]" + {{- end }} {{- if $serviceSpec.envFromSecret }} envFrom: - secretRef: name: {{ $serviceSpec.envFromSecret }} {{- end }} + {{- if $serviceSpec.componentType | eq "frontend" }} + ports: + - name: http + containerPort: {{ $.Values.dynamoPort | default 8000 }} + protocol: TCP + {{- else if $serviceSpec.componentType | eq "worker" }} ports: - - name: health - containerPort: {{ $.Values.healthPort | default 5000 }} + - name: system + containerPort: {{ $.Values.dynamoSystemPort | default 9090 }} + protocol: TCP + {{- end }} + {{- if and $serviceSpec.componentType (or (eq $serviceSpec.componentType "frontend") (eq $serviceSpec.componentType "worker")) }} livenessProbe: {{- if $serviceSpec.livenessProbe }} - {{ $serviceSpec.livenessProbe | toYaml | nindent 14 }} + {{ $serviceSpec.livenessProbe | toYaml | nindent 10 }} {{- else }} initialDelaySeconds: 60 periodSeconds: 60 timeoutSeconds: 5 failureThreshold: 10 successThreshold: 1 + {{- if $serviceSpec.componentType | eq "frontend" }} + httpGet: + path: /health + port: http + {{- else if $serviceSpec.componentType | eq "worker" }} + httpGet: + path: /live + port: system + {{- else }} httpGet: path: /healthz port: health scheme: HTTP {{- end }} + {{- end }} readinessProbe: {{- if $serviceSpec.readinessProbe }} - {{ $serviceSpec.readinessProbe | toYaml | nindent 14 }} + {{ $serviceSpec.readinessProbe | toYaml | nindent 10 }} {{- else }} initialDelaySeconds: 60 periodSeconds: 60 timeoutSeconds: 5 failureThreshold: 10 successThreshold: 1 + {{- if $serviceSpec.componentType | eq "frontend" }} + exec: + command: + - /bin/sh + - -c + - curl -s http://localhost:${DYNAMO_PORT}/health | jq -e ".status == \"healthy\"" + {{- else if $serviceSpec.componentType | eq "worker" }} + httpGet: + path: /health + port: system + {{- else }} httpGet: path: /readyz port: health scheme: HTTP {{- end }} + {{- end }} + {{- end }} {{- end }} {{- end }} diff --git a/deploy/helm/chart/templates/service.yaml b/deploy/helm/chart/templates/service.yaml index 525980e571..ee077172ff 100644 --- a/deploy/helm/chart/templates/service.yaml +++ b/deploy/helm/chart/templates/service.yaml @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. {{- range $serviceName, $serviceSpec := .Values.spec.services }} -{{- if eq $serviceSpec.componentType "main" }} +{{- if eq $serviceSpec.componentType "frontend" }} --- apiVersion: v1 kind: Service From cb59d0e141d50cf7342daff0e57a435d34423dff Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Sun, 24 Aug 2025 07:40:50 -0600 Subject: [PATCH 16/82] fix: do not fail if backendFramework cannot be detected (#2655) Signed-off-by: Jason Zhou --- .../cloud/operator/internal/dynamo/graph.go | 8 ++--- .../operator/internal/dynamo/graph_test.go | 36 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/deploy/cloud/operator/internal/dynamo/graph.go b/deploy/cloud/operator/internal/dynamo/graph.go index 0e8b473578..ff83faa5a6 100644 --- a/deploy/cloud/operator/internal/dynamo/graph.go +++ b/deploy/cloud/operator/internal/dynamo/graph.go @@ -1012,7 +1012,7 @@ func detectBackendFrameworkFromArgs(command []string, args []string) (BackendFra } if len(detected) == 0 { - return "", fmt.Errorf("no backend framework detected from command: %q", fullCommand) + return BackendFrameworkNoop, nil } if len(detected) > 1 { @@ -1059,13 +1059,13 @@ func determineBackendFramework( } // Validate consistency if both detected and explicit exist - if detectedFramework != "" && explicitFramework != "" && detectedFramework != explicitFramework { + if detectedFramework != "" && detectedFramework != BackendFrameworkNoop && explicitFramework != "" && detectedFramework != explicitFramework { return "", fmt.Errorf("backend framework mismatch: detected %q from command but explicitly configured as %q", detectedFramework, explicitFramework) } // Return in order of preference: detected > explicit > error - if detectedFramework != "" { + if detectedFramework != "" && detectedFramework != BackendFrameworkNoop { return detectedFramework, nil } @@ -1079,7 +1079,7 @@ func determineBackendFramework( } // No command/args to detect from and no explicit config - return "", fmt.Errorf("backend framework must be specified explicitly or detectable from command/args") + return BackendFrameworkNoop, nil } // getBackendFrameworkFromComponent attempts to determine backend framework using hybrid approach: diff --git a/deploy/cloud/operator/internal/dynamo/graph_test.go b/deploy/cloud/operator/internal/dynamo/graph_test.go index 496c34efef..4fc602233f 100644 --- a/deploy/cloud/operator/internal/dynamo/graph_test.go +++ b/deploy/cloud/operator/internal/dynamo/graph_test.go @@ -3626,10 +3626,10 @@ func TestDetectBackendFrameworkFromArgs(t *testing.T) { expected: BackendFrameworkSGLang, }, { - name: "no backend detected", - command: []string{"/bin/sh", "-c"}, - args: []string{"echo hello world"}, - expectError: true, + name: "no backend detected", + command: []string{"/bin/sh", "-c"}, + args: []string{"echo hello world"}, + expected: BackendFrameworkNoop, }, { name: "multiple backends detected", @@ -3709,17 +3709,17 @@ func TestDetermineBackendFramework(t *testing.T) { errorContains: "backend framework mismatch", }, { - name: "worker with no detection, no explicit - returns error", + name: "worker with no detection, no explicit - returns noop", componentType: "worker", - expectError: true, - errorContains: "backend framework must be specified explicitly or detectable from command/args", + expected: BackendFrameworkNoop, + expectError: false, }, { - name: "worker with detection failure, no explicit - returns error", + name: "worker with detection failure, no explicit - returns noop", componentType: "worker", args: []string{"echo hello world"}, - expectError: true, - errorContains: "could not determine backend framework", + expected: BackendFrameworkNoop, + expectError: false, }, } @@ -3843,18 +3843,18 @@ func TestGetBackendFrameworkFromComponent(t *testing.T) { expected: BackendFrameworkNoop, }, { - name: "worker with no detection, no explicit - returns error", + name: "worker with no detection, no explicit - returns noop", component: &v1alpha1.DynamoComponentDeploymentOverridesSpec{ DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{ ComponentType: "worker", // Worker component }, }, - deployment: &v1alpha1.DynamoGraphDeployment{}, - expectError: true, - errorContains: "backend framework must be specified explicitly or detectable from command/args", + deployment: &v1alpha1.DynamoGraphDeployment{}, + expected: BackendFrameworkNoop, + expectError: false, }, { - name: "worker with detection failure, no explicit - returns error", + name: "worker with detection failure, no explicit - returns noop", component: &v1alpha1.DynamoComponentDeploymentOverridesSpec{ DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{ ComponentType: "worker", // Worker component @@ -3865,9 +3865,9 @@ func TestGetBackendFrameworkFromComponent(t *testing.T) { }, }, }, - deployment: &v1alpha1.DynamoGraphDeployment{}, - expectError: true, - errorContains: "could not determine backend framework", + deployment: &v1alpha1.DynamoGraphDeployment{}, + expected: BackendFrameworkNoop, + expectError: false, }, } From 024d0f43a54cfdca07fb59497bd0fe2bf0722297 Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Sun, 24 Aug 2025 07:41:32 -0600 Subject: [PATCH 17/82] fix: fix env vars override (#2640) Signed-off-by: Jason Zhou --- ...namocomponentdeployment_controller_test.go | 28 +++- .../cloud/operator/internal/dynamo/graph.go | 4 +- .../operator/internal/dynamo/graph_test.go | 137 ++++++++++++++++++ 3 files changed, 166 insertions(+), 3 deletions(-) diff --git a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go index d00d8a5cd8..e4ed8b16f3 100644 --- a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go +++ b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go @@ -681,6 +681,12 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. ObjectMeta: metav1.ObjectMeta{ Name: "test-lws-deploy", Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "DynamoGraphDeployment", + Name: "test-lws-deploy", + }, + }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ DynamoComponent: "test-lws-component", @@ -805,7 +811,16 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. Image: "test-image:latest", Command: []string{"sh", "-c"}, Args: []string{"ray start --head --port=6379 && some dynamo command"}, - Env: []corev1.EnvVar{{Name: "TEST_ENV_FROM_DYNAMO_COMPONENT_DEPLOYMENT_SPEC", Value: "test_value_from_dynamo_component_deployment_spec"}, {Name: "TEST_ENV_FROM_EXTRA_POD_SPEC", Value: "test_value_from_extra_pod_spec"}}, + Env: []corev1.EnvVar{ + {Name: "DYN_NAMESPACE", Value: "default"}, + {Name: "DYN_PARENT_DGD_K8S_NAME", Value: "test-lws-deploy"}, + {Name: "DYN_PARENT_DGD_K8S_NAMESPACE", Value: "default"}, + {Name: "DYN_SYSTEM_ENABLED", Value: "true"}, + {Name: "DYN_SYSTEM_PORT", Value: "9090"}, + {Name: "DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS", Value: `["generate"]`}, + {Name: "TEST_ENV_FROM_DYNAMO_COMPONENT_DEPLOYMENT_SPEC", Value: "test_value_from_dynamo_component_deployment_spec"}, + {Name: "TEST_ENV_FROM_EXTRA_POD_SPEC", Value: "test_value_from_extra_pod_spec"}, + }, Ports: []corev1.ContainerPort{ { Protocol: corev1.ProtocolTCP, Name: commonconsts.DynamoSystemPortName, ContainerPort: commonconsts.DynamoSystemPort, @@ -905,7 +920,16 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. Image: "test-image:latest", Command: []string{"sh", "-c"}, Args: []string{"ray start --address=${LWS_LEADER_ADDRESS}:6379 --block"}, - Env: []corev1.EnvVar{{Name: "TEST_ENV_FROM_DYNAMO_COMPONENT_DEPLOYMENT_SPEC", Value: "test_value_from_dynamo_component_deployment_spec"}, {Name: "TEST_ENV_FROM_EXTRA_POD_SPEC", Value: "test_value_from_extra_pod_spec"}}, + Env: []corev1.EnvVar{ + {Name: "DYN_NAMESPACE", Value: "default"}, + {Name: "DYN_PARENT_DGD_K8S_NAME", Value: "test-lws-deploy"}, + {Name: "DYN_PARENT_DGD_K8S_NAMESPACE", Value: "default"}, + {Name: "DYN_SYSTEM_ENABLED", Value: "true"}, + {Name: "DYN_SYSTEM_PORT", Value: "9090"}, + {Name: "DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS", Value: `["generate"]`}, + {Name: "TEST_ENV_FROM_DYNAMO_COMPONENT_DEPLOYMENT_SPEC", Value: "test_value_from_dynamo_component_deployment_spec"}, + {Name: "TEST_ENV_FROM_EXTRA_POD_SPEC", Value: "test_value_from_extra_pod_spec"}, + }, Ports: []corev1.ContainerPort{ { Protocol: corev1.ProtocolTCP, Name: commonconsts.DynamoSystemPortName, ContainerPort: commonconsts.DynamoSystemPort, diff --git a/deploy/cloud/operator/internal/dynamo/graph.go b/deploy/cloud/operator/internal/dynamo/graph.go index ff83faa5a6..1cf01def25 100644 --- a/deploy/cloud/operator/internal/dynamo/graph.go +++ b/deploy/cloud/operator/internal/dynamo/graph.go @@ -701,13 +701,14 @@ func GenerateBasePodSpec( main := component.ExtraPodSpec.MainContainer.DeepCopy() if main != nil { // merge the extraPodSpec from the parent deployment with the extraPodSpec from the service + containerEnvs := container.Env err = mergo.Merge(&container, *main, mergo.WithOverride) if err != nil { return nil, fmt.Errorf("failed to merge extraPodSpec: %w", err) } // main container fields that require special handling - container.Env = MergeEnvs(component.Envs, container.Env) + container.Env = MergeEnvs(containerEnvs, container.Env) // Note: startup probe does not have its own top level field so it must be passed in extraPodSpec.MainContainer // We want to overwrite entirely if provided rather than merge if main.StartupProbe != nil { @@ -715,6 +716,7 @@ func GenerateBasePodSpec( } } } + container.Env = MergeEnvs(component.Envs, container.Env) // Merge probes entirely if they are passed (no partial merge) if component.LivenessProbe != nil { diff --git a/deploy/cloud/operator/internal/dynamo/graph_test.go b/deploy/cloud/operator/internal/dynamo/graph_test.go index 4fc602233f..92107de835 100644 --- a/deploy/cloud/operator/internal/dynamo/graph_test.go +++ b/deploy/cloud/operator/internal/dynamo/graph_test.go @@ -4392,3 +4392,140 @@ func TestGenerateBasePodSpec_PlannerServiceAccount(t *testing.T) { }) } } + +func TestGenerateBasePodSpec_Worker(t *testing.T) { + secretsRetriever := &mockSecretsRetriever{} + controllerConfig := controller_common.Config{} + + tests := []struct { + name string + component *v1alpha1.DynamoComponentDeploymentOverridesSpec + expectedPodSpec *corev1.PodSpec + }{ + { + name: "Planner component should have planner service account", + component: &v1alpha1.DynamoComponentDeploymentOverridesSpec{ + DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{ + Envs: []corev1.EnvVar{ + {Name: "ANOTHER_COMPONENTENV", Value: "true"}, + }, + ComponentType: commonconsts.ComponentTypeWorker, + ExtraPodSpec: &common.ExtraPodSpec{ + MainContainer: &corev1.Container{ + Command: []string{"python3"}, + Args: []string{"-m", "dynamo.worker"}, + Env: []corev1.EnvVar{ + {Name: "ANOTHER_CONTAINER_ENV", Value: "true"}, + }, + }, + }, + }, + }, + expectedPodSpec: &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "main", + Command: []string{"python3"}, + Args: []string{"-m", "dynamo.worker"}, + Env: []corev1.EnvVar{ + {Name: "ANOTHER_COMPONENTENV", Value: "true"}, + {Name: "ANOTHER_CONTAINER_ENV", Value: "true"}, + {Name: "DYN_NAMESPACE", Value: ""}, + {Name: "DYN_PARENT_DGD_K8S_NAME", Value: "test-deployment"}, + {Name: "DYN_PARENT_DGD_K8S_NAMESPACE", Value: "default"}, + {Name: "DYN_SYSTEM_ENABLED", Value: "true"}, + {Name: "DYN_SYSTEM_PORT", Value: "9090"}, + {Name: "DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS", Value: "[\"generate\"]"}, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "shared-memory", + MountPath: "/dev/shm", + }, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Port: intstr.FromString(commonconsts.DynamoSystemPortName), + }, + }, + PeriodSeconds: 5, + TimeoutSeconds: 30, + FailureThreshold: 1, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/health", + Port: intstr.FromString(commonconsts.DynamoSystemPortName), + }, + }, + PeriodSeconds: 10, + TimeoutSeconds: 30, + FailureThreshold: 60, + }, + StartupProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Port: intstr.FromString(commonconsts.DynamoSystemPortName), + }, + }, + PeriodSeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 60, + }, + Ports: []corev1.ContainerPort{ + { + Name: commonconsts.DynamoSystemPortName, + ContainerPort: int32(commonconsts.DynamoSystemPort), + Protocol: corev1.ProtocolTCP, + }, + }, + }, + }, + RestartPolicy: corev1.RestartPolicyAlways, + TerminationGracePeriodSeconds: ptr.To(int64(60)), + Volumes: []corev1.Volume{ + { + Name: "shared-memory", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + SizeLimit: func() *resource.Quantity { q := resource.MustParse("512Mi"); return &q }(), + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + podSpec, err := GenerateBasePodSpec( + tt.component, + BackendFrameworkSGLang, + secretsRetriever, + "test-deployment", + "default", + RoleMain, + 1, + controllerConfig, + commonconsts.MultinodeDeploymentTypeGrove, + "test-service", + ) + + if err != nil { + t.Errorf("GenerateBasePodSpec() error = %v", err) + return + } + + diff := cmp.Diff(tt.expectedPodSpec, podSpec) + if diff != "" { + t.Errorf("GenerateBasePodSpec() podSpec = %v, want %v, diff = %v", podSpec, tt.expectedPodSpec, diff) + } + }) + } +} From dbaaf3bbe1abf5d4efee8acdc1e360f79f0313bb Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Sun, 24 Aug 2025 07:42:26 -0600 Subject: [PATCH 18/82] fix: increase shm default size and make it configurable (#2616) Signed-off-by: Jason Zhou --- ...nvidia.com_dynamocomponentdeployments.yaml | 12 ++++++ .../nvidia.com_dynamographdeployments.yaml | 12 ++++++ deploy/cloud/operator/api/v1alpha1/common.go | 5 +++ .../dynamocomponentdeployment_types.go | 3 ++ .../api/v1alpha1/zz_generated.deepcopy.go | 21 ++++++++++ ...nvidia.com_dynamocomponentdeployments.yaml | 12 ++++++ .../nvidia.com_dynamographdeployments.yaml | 12 ++++++ .../cloud/operator/internal/consts/consts.go | 8 ++-- ...namocomponentdeployment_controller_test.go | 25 ++++++------ .../cloud/operator/internal/dynamo/graph.go | 40 +++++++++---------- .../operator/internal/dynamo/graph_test.go | 40 +++++++++---------- 11 files changed, 132 insertions(+), 58 deletions(-) diff --git a/deploy/cloud/helm/crds/templates/nvidia.com_dynamocomponentdeployments.yaml b/deploy/cloud/helm/crds/templates/nvidia.com_dynamocomponentdeployments.yaml index ffb4275fb7..f0752eed8d 100644 --- a/deploy/cloud/helm/crds/templates/nvidia.com_dynamocomponentdeployments.yaml +++ b/deploy/cloud/helm/crds/templates/nvidia.com_dynamocomponentdeployments.yaml @@ -10241,6 +10241,18 @@ spec: serviceName: description: contains the name of the component type: string + sharedMemory: + description: SharedMemory controls the tmpfs mounted at /dev/shm (enable/disable and size). + properties: + disabled: + type: boolean + size: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object type: object status: description: Status reflects the current observed state of the component deployment. diff --git a/deploy/cloud/helm/crds/templates/nvidia.com_dynamographdeployments.yaml b/deploy/cloud/helm/crds/templates/nvidia.com_dynamographdeployments.yaml index 3e3c8c82bc..9ecebe7cf9 100644 --- a/deploy/cloud/helm/crds/templates/nvidia.com_dynamographdeployments.yaml +++ b/deploy/cloud/helm/crds/templates/nvidia.com_dynamographdeployments.yaml @@ -10340,6 +10340,18 @@ spec: serviceName: description: contains the name of the component type: string + sharedMemory: + description: SharedMemory controls the tmpfs mounted at /dev/shm (enable/disable and size). + properties: + disabled: + type: boolean + size: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object type: object description: |- Services allows per-service overrides of the component deployment settings. diff --git a/deploy/cloud/operator/api/v1alpha1/common.go b/deploy/cloud/operator/api/v1alpha1/common.go index bacd70aabb..47132b5c60 100644 --- a/deploy/cloud/operator/api/v1alpha1/common.go +++ b/deploy/cloud/operator/api/v1alpha1/common.go @@ -44,3 +44,8 @@ type Autoscaling struct { Behavior *autoscalingv2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"` Metrics []autoscalingv2.MetricSpec `json:"metrics,omitempty"` } + +type SharedMemorySpec struct { + Disabled bool `json:"disabled,omitempty"` + Size resource.Quantity `json:"size,omitempty"` +} diff --git a/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go b/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go index dcf11ebbe2..0e99bef761 100644 --- a/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go +++ b/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go @@ -92,6 +92,9 @@ type DynamoComponentDeploymentSharedSpec struct { // Ingress config to expose the component outside the cluster (or through a service mesh). Ingress *IngressSpec `json:"ingress,omitempty"` + // SharedMemory controls the tmpfs mounted at /dev/shm (enable/disable and size). + SharedMemory *SharedMemorySpec `json:"sharedMemory,omitempty"` + // +optional // ExtraPodMetadata adds labels/annotations to the created Pods. ExtraPodMetadata *dynamoCommon.ExtraPodMetadata `json:"extraPodMetadata,omitempty"` diff --git a/deploy/cloud/operator/api/v1alpha1/zz_generated.deepcopy.go b/deploy/cloud/operator/api/v1alpha1/zz_generated.deepcopy.go index 5378c152b7..b9734d8f21 100644 --- a/deploy/cloud/operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/deploy/cloud/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -243,6 +243,11 @@ func (in *DynamoComponentDeploymentSharedSpec) DeepCopyInto(out *DynamoComponent *out = new(IngressSpec) (*in).DeepCopyInto(*out) } + if in.SharedMemory != nil { + in, out := &in.SharedMemory, &out.SharedMemory + *out = new(SharedMemorySpec) + (*in).DeepCopyInto(*out) + } if in.ExtraPodMetadata != nil { in, out := &in.ExtraPodMetadata, &out.ExtraPodMetadata *out = new(common.ExtraPodMetadata) @@ -563,3 +568,19 @@ func (in *PVC) DeepCopy() *PVC { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SharedMemorySpec) DeepCopyInto(out *SharedMemorySpec) { + *out = *in + out.Size = in.Size.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharedMemorySpec. +func (in *SharedMemorySpec) DeepCopy() *SharedMemorySpec { + if in == nil { + return nil + } + out := new(SharedMemorySpec) + in.DeepCopyInto(out) + return out +} diff --git a/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml b/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml index ffb4275fb7..f0752eed8d 100644 --- a/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml +++ b/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamocomponentdeployments.yaml @@ -10241,6 +10241,18 @@ spec: serviceName: description: contains the name of the component type: string + sharedMemory: + description: SharedMemory controls the tmpfs mounted at /dev/shm (enable/disable and size). + properties: + disabled: + type: boolean + size: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object type: object status: description: Status reflects the current observed state of the component deployment. diff --git a/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml b/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml index 3e3c8c82bc..9ecebe7cf9 100644 --- a/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml +++ b/deploy/cloud/operator/config/crd/bases/nvidia.com_dynamographdeployments.yaml @@ -10340,6 +10340,18 @@ spec: serviceName: description: contains the name of the component type: string + sharedMemory: + description: SharedMemory controls the tmpfs mounted at /dev/shm (enable/disable and size). + properties: + disabled: + type: boolean + size: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object type: object description: |- Services allows per-service overrides of the component deployment settings. diff --git a/deploy/cloud/operator/internal/consts/consts.go b/deploy/cloud/operator/internal/consts/consts.go index 453d71541f..97b0aeeb85 100644 --- a/deploy/cloud/operator/internal/consts/consts.go +++ b/deploy/cloud/operator/internal/consts/consts.go @@ -48,9 +48,11 @@ const ( DefaultGroveTerminationDelay = 15 * time.Minute // Metrics related constants - KubeAnnotationEnableMetrics = "nvidia.com/enable-metrics" // User-provided annotation to control metrics - KubeLabelMetricsEnabled = "nvidia.com/metrics-enabled" // Controller-managed label for pod selection - KubeValueNameSharedMemory = "shared-memory" + KubeAnnotationEnableMetrics = "nvidia.com/enable-metrics" // User-provided annotation to control metrics + KubeLabelMetricsEnabled = "nvidia.com/metrics-enabled" // Controller-managed label for pod selection + KubeValueNameSharedMemory = "shared-memory" + DefaultSharedMemoryMountPath = "/dev/shm" + DefaultSharedMemorySize = "8Gi" // Grove multinode role suffixes GroveRoleSuffixLeader = "ldr" diff --git a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go index e4ed8b16f3..0a85077ab9 100644 --- a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go +++ b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go @@ -24,7 +24,6 @@ import ( "fmt" "testing" - "github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common" dynamoCommon "github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common" "github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/v1alpha1" commonconsts "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts" @@ -705,18 +704,18 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. Multinode: &v1alpha1.MultinodeSpec{ NodeCount: 2, }, - Resources: &common.Resources{ - Requests: &common.ResourceItem{ + Resources: &dynamoCommon.Resources{ + Requests: &dynamoCommon.ResourceItem{ CPU: "300m", Memory: "500Mi", }, - Limits: &common.ResourceItem{ + Limits: &dynamoCommon.ResourceItem{ GPU: "1", Memory: "20Gi", CPU: "10", }, }, - ExtraPodMetadata: &common.ExtraPodMetadata{ + ExtraPodMetadata: &dynamoCommon.ExtraPodMetadata{ Annotations: map[string]string{ "nvidia.com/annotation1": "annotation1", }, @@ -799,7 +798,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(5*1024*1024*1024, resource.BinarySI), // 5gi (calculated from memory limit / 4) + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -829,7 +828,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. VolumeMounts: []corev1.VolumeMount{ { Name: "shared-memory", - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, Resources: corev1.ResourceRequirements{ @@ -908,7 +907,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(5*1024*1024*1024, resource.BinarySI), // 5gi (calculated from memory limit / 4) + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -938,7 +937,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. VolumeMounts: []corev1.VolumeMount{ { Name: "shared-memory", - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, Resources: corev1.ResourceRequirements{ @@ -980,8 +979,8 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. Multinode: &v1alpha1.MultinodeSpec{ NodeCount: 2, }, - Resources: &common.Resources{ - Limits: &common.ResourceItem{ + Resources: &dynamoCommon.Resources{ + Limits: &dynamoCommon.ResourceItem{ GPU: "1", }, }, @@ -1024,8 +1023,8 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. Multinode: &v1alpha1.MultinodeSpec{ NodeCount: 2, }, - Resources: &common.Resources{ - Limits: &common.ResourceItem{ + Resources: &dynamoCommon.Resources{ + Limits: &dynamoCommon.ResourceItem{ GPU: "1", }, }, diff --git a/deploy/cloud/operator/internal/dynamo/graph.go b/deploy/cloud/operator/internal/dynamo/graph.go index 1cf01def25..9f3c596294 100644 --- a/deploy/cloud/operator/internal/dynamo/graph.go +++ b/deploy/cloud/operator/internal/dynamo/graph.go @@ -677,6 +677,8 @@ func addStandardEnvVars(container *corev1.Container, controllerConfig controller // GenerateBasePodSpec creates a basic PodSpec with common logic shared between controller and grove // Includes standard environment variables (DYNAMO_PORT, NATS_SERVER, ETCD_ENDPOINTS) // Deployment-specific environment merging should be handled by the caller +// +//nolint:gocyclo func GenerateBasePodSpec( component *v1alpha1.DynamoComponentDeploymentOverridesSpec, backendFramework BackendFramework, @@ -780,9 +782,10 @@ func GenerateBasePodSpec( MountPath: *component.PVC.MountPoint, }) } - shmVolume, shmVolumeMount := generateSharedMemoryVolumeAndMount(&container.Resources) - volumes = append(volumes, shmVolume) - container.VolumeMounts = append(container.VolumeMounts, shmVolumeMount) + if shmVol, shmMount := generateSharedMemoryVolumeAndMount(component.SharedMemory); shmVol != nil && shmMount != nil { + volumes = append(volumes, *shmVol) + container.VolumeMounts = append(container.VolumeMounts, *shmMount) + } // Apply backend-specific container modifications multinodeDeployer := MultinodeDeployerFactory(multinodeDeploymentType) @@ -1181,36 +1184,29 @@ func GenerateBasePodSpecForController( return podSpec, nil } -func generateSharedMemoryVolumeAndMount(resources *corev1.ResourceRequirements) (corev1.Volume, corev1.VolumeMount) { - sharedMemorySizeLimit := resource.MustParse("512Mi") - // Check if we have memory limits to work with - memoryLimit := resources.Limits[corev1.ResourceMemory] - if !memoryLimit.IsZero() { - // Use 1/4 of memory limit - calculatedSize := resource.NewQuantity(memoryLimit.Value()/4, resource.BinarySI) - // Apply bounds: minimum 512Mi, maximum 8Gi - minSize := resource.MustParse("512Mi") - maxSize := resource.MustParse("8Gi") - - if calculatedSize.Cmp(minSize) > 0 && calculatedSize.Cmp(maxSize) < 0 { - sharedMemorySizeLimit = *calculatedSize - } else if calculatedSize.Cmp(maxSize) >= 0 { - sharedMemorySizeLimit = maxSize // Cap at maximum +func generateSharedMemoryVolumeAndMount(spec *v1alpha1.SharedMemorySpec) (*corev1.Volume, *corev1.VolumeMount) { + // default: enabled=true, size=8Gi + size := resource.MustParse(commonconsts.DefaultSharedMemorySize) + if spec != nil { + if spec.Disabled { + return nil, nil + } + if !spec.Size.IsZero() { + size = spec.Size } - // If calculatedSize < minSize, keep the 512Mi base } volume := corev1.Volume{ Name: commonconsts.KubeValueNameSharedMemory, VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: &sharedMemorySizeLimit, + SizeLimit: &size, }, }, } volumeMount := corev1.VolumeMount{ Name: commonconsts.KubeValueNameSharedMemory, - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, } - return volume, volumeMount + return &volume, &volumeMount } diff --git a/deploy/cloud/operator/internal/dynamo/graph_test.go b/deploy/cloud/operator/internal/dynamo/graph_test.go index 92107de835..ba58d916d2 100644 --- a/deploy/cloud/operator/internal/dynamo/graph_test.go +++ b/deploy/cloud/operator/internal/dynamo/graph_test.go @@ -1235,7 +1235,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(536870912, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -1337,7 +1337,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeMounts: []corev1.VolumeMount{ { Name: "shared-memory", - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, Ports: []corev1.ContainerPort{ @@ -1378,7 +1378,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(536870912, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -1471,7 +1471,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { }, { Name: "shared-memory", - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, }, @@ -1733,7 +1733,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -1812,7 +1812,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeMounts: []corev1.VolumeMount{ { Name: commonconsts.KubeValueNameSharedMemory, - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, LivenessProbe: &corev1.Probe{ @@ -1883,7 +1883,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -1962,7 +1962,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeMounts: []corev1.VolumeMount{ { Name: commonconsts.KubeValueNameSharedMemory, - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, }, @@ -1989,7 +1989,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -2098,7 +2098,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeMounts: []corev1.VolumeMount{ { Name: commonconsts.KubeValueNameSharedMemory, - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, }, @@ -2134,7 +2134,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -2225,7 +2225,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { }, { Name: "shared-memory", - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, }, @@ -2509,7 +2509,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -2590,7 +2590,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeMounts: []corev1.VolumeMount{ { Name: commonconsts.KubeValueNameSharedMemory, - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, ReadinessProbe: &corev1.Probe{ @@ -2648,7 +2648,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -2728,7 +2728,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeMounts: []corev1.VolumeMount{ { Name: commonconsts.KubeValueNameSharedMemory, - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, }, @@ -2755,7 +2755,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -2864,7 +2864,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeMounts: []corev1.VolumeMount{ { Name: commonconsts.KubeValueNameSharedMemory, - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, }, @@ -2899,7 +2899,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: resource.NewQuantity(512*1024*1024, resource.BinarySI), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, @@ -2991,7 +2991,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) { }, { Name: "shared-memory", - MountPath: "/dev/shm", + MountPath: commonconsts.DefaultSharedMemoryMountPath, }, }, }, From 2a56c5a879ff1d51757f16852887f466c8d28134 Mon Sep 17 00:00:00 2001 From: Alec <35311602+alec-flowers@users.noreply.github.com> Date: Sun, 24 Aug 2025 10:13:41 -0700 Subject: [PATCH 19/82] fix: pytest robustness and parsing error (#2676) Signed-off-by: Jason Zhou --- .../multimodal/components/encode_worker.py | 36 ++-- examples/multimodal/components/processor.py | 38 ++-- lib/bindings/python/src/dynamo/_core.pyi | 18 ++ tests/serve/common.py | 58 ++++++ tests/serve/test_trtllm.py | 152 ++-------------- tests/serve/test_vllm.py | 171 +++--------------- tests/utils/deployment_graph.py | 37 +++- tests/utils/engine_process.py | 164 +++++++++++++++++ tests/utils/managed_process.py | 97 +++++++++- 9 files changed, 449 insertions(+), 322 deletions(-) create mode 100644 tests/serve/common.py create mode 100644 tests/utils/engine_process.py diff --git a/examples/multimodal/components/encode_worker.py b/examples/multimodal/components/encode_worker.py index 5b743d1275..904434c33f 100644 --- a/examples/multimodal/components/encode_worker.py +++ b/examples/multimodal/components/encode_worker.py @@ -28,7 +28,7 @@ from vllm.utils import FlexibleArgumentParser import dynamo.nixl_connect as connect -from dynamo.runtime import DistributedRuntime, dynamo_worker +from dynamo.runtime import Client, DistributedRuntime, dynamo_worker from dynamo.runtime.logging import configure_dynamo_logging sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) @@ -56,8 +56,13 @@ class VllmEncodeWorker: - def __init__(self, args: argparse.Namespace, engine_args: AsyncEngineArgs) -> None: - self.downstream_endpoint = args.downstream_endpoint + def __init__( + self, + args: argparse.Namespace, + engine_args: AsyncEngineArgs, + pd_worker_client: Client, + ) -> None: + self.pd_worker_client = pd_worker_client self.engine_args = engine_args self.model = self.engine_args.model @@ -178,16 +183,6 @@ async def generate( async def async_init(self, runtime: DistributedRuntime): logger.info("Startup started.") - parsed_namespace, parsed_component_name, parsed_endpoint_name = parse_endpoint( - self.downstream_endpoint - ) - self.pd_worker_client = ( - await runtime.namespace(parsed_namespace) - .component(parsed_component_name) - .endpoint(parsed_endpoint_name) - .client() - ) - # Create and initialize a dynamo connector for this worker. # We'll needs this to move data between this worker and remote workers efficiently. self._connector = connect.Connector() @@ -262,9 +257,22 @@ async def init(runtime: DistributedRuntime, args: argparse.Namespace, config: Co generate_endpoint = component.endpoint(config.endpoint) - handler = VllmEncodeWorker(args, config.engine_args) + parsed_namespace, parsed_component_name, parsed_endpoint_name = parse_endpoint( + args.downstream_endpoint + ) + pd_worker_client = ( + await runtime.namespace(parsed_namespace) + .component(parsed_component_name) + .endpoint(parsed_endpoint_name) + .client() + ) + + handler = VllmEncodeWorker(args, config.engine_args, pd_worker_client) await handler.async_init(runtime) + logger.info("Waiting for PD Worker Instances ...") + await pd_worker_client.wait_for_instances() + logger.info(f"Starting to serve the {args.endpoint} endpoint...") try: diff --git a/examples/multimodal/components/processor.py b/examples/multimodal/components/processor.py index 12de2a97b6..d155a033bb 100644 --- a/examples/multimodal/components/processor.py +++ b/examples/multimodal/components/processor.py @@ -33,7 +33,7 @@ from vllm.utils import FlexibleArgumentParser from dynamo.llm import ModelType, register_llm -from dynamo.runtime import DistributedRuntime, dynamo_worker +from dynamo.runtime import Client, DistributedRuntime, dynamo_worker from dynamo.runtime.logging import configure_dynamo_logging # To import example local module @@ -96,9 +96,14 @@ def parse_args(cls) -> Tuple[argparse.Namespace, Config]: return args, config - def __init__(self, args: argparse.Namespace, engine_args: AsyncEngineArgs): + def __init__( + self, + args: argparse.Namespace, + engine_args: AsyncEngineArgs, + encode_worker_client: Client, + ): + self.encode_worker_client = encode_worker_client self.prompt_template = args.prompt_template - self.downstream_endpoint = args.downstream_endpoint self.engine_args = engine_args self.model_config = self.engine_args.create_model_config() self.default_sampling_params = self.model_config.get_diff_sampling_param() @@ -125,17 +130,6 @@ def _create_tokenizer(self, engine_args: AsyncEngineArgs) -> AnyTokenizer: ) return base_tokenizer - async def async_init(self, runtime: DistributedRuntime): - parsed_namespace, parsed_component_name, parsed_endpoint_name = parse_endpoint( - self.downstream_endpoint - ) - self.encode_worker_client = ( - await runtime.namespace(parsed_namespace) - .component(parsed_component_name) - .endpoint(parsed_endpoint_name) - .client() - ) - # Main method to parse the request and send the request to the vllm worker. async def _generate( self, @@ -300,8 +294,20 @@ async def init(runtime: DistributedRuntime, args: argparse.Namespace, config: Co generate_endpoint = component.endpoint(config.endpoint) - handler = Processor(args, config.engine_args) - await handler.async_init(runtime) + parsed_namespace, parsed_component_name, parsed_endpoint_name = parse_endpoint( + args.downstream_endpoint + ) + encode_worker_client = ( + await runtime.namespace(parsed_namespace) + .component(parsed_component_name) + .endpoint(parsed_endpoint_name) + .client() + ) + + handler = Processor(args, config.engine_args, encode_worker_client) + + logger.info("Waiting for Encoder Worker Instances ...") + await encode_worker_client.wait_for_instances() # Register the endpoint as entrypoint to a model await register_llm( diff --git a/lib/bindings/python/src/dynamo/_core.pyi b/lib/bindings/python/src/dynamo/_core.pyi index 0acafc1cb6..73e89a5f34 100644 --- a/lib/bindings/python/src/dynamo/_core.pyi +++ b/lib/bindings/python/src/dynamo/_core.pyi @@ -246,6 +246,24 @@ class Client: ... + def instance_ids(self) -> List[int]: + """ + Get list of current instance IDs. + + Returns: + A list of currently available instance IDs + """ + ... + + async def wait_for_instances(self) -> List[int]: + """ + Wait for instances to be available for work and return their IDs. + + Returns: + A list of instance IDs that are available for work + """ + ... + async def random(self, request: JsonLike) -> AsyncIterator[JsonLike]: """ Pick a random instance of the endpoint and issue the request diff --git a/tests/serve/common.py b/tests/serve/common.py new file mode 100644 index 0000000000..68e79d680a --- /dev/null +++ b/tests/serve/common.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Common base classes and utilities for engine tests (vLLM, TRT-LLM, etc.)""" + +from dataclasses import dataclass +from typing import Any, Callable, List + +from tests.utils.deployment_graph import Payload + +# Common text prompt used across tests +TEXT_PROMPT = "Tell me a short joke about AI." + + +@dataclass +class EngineConfig: + """Base configuration for engine test scenarios""" + + name: str + directory: str + script_name: str + marks: List[Any] + endpoints: List[str] + response_handlers: List[Callable[[Any], str]] + model: str + timeout: int = 120 + delayed_start: int = 0 + + +def create_payload_for_config(config: EngineConfig) -> Payload: + """Create a standard payload using the model from the engine config. + + This provides the default implementation for text-only models. + """ + return Payload( + payload_chat={ + "model": config.model, + "messages": [ + { + "role": "user", + "content": TEXT_PROMPT, + } + ], + "max_tokens": 150, + "temperature": 0.1, + "stream": False, + }, + payload_completions={ + "model": config.model, + "prompt": TEXT_PROMPT, + "max_tokens": 150, + "temperature": 0.1, + "stream": False, + }, + repeat_count=3, + expected_log=[], + expected_response=["AI"], + ) diff --git a/tests/serve/test_trtllm.py b/tests/serve/test_trtllm.py index 90153b8b96..898c3e3b6a 100644 --- a/tests/serve/test_trtllm.py +++ b/tests/serve/test_trtllm.py @@ -5,66 +5,27 @@ import os import time from dataclasses import dataclass -from typing import Any, Callable, List import pytest -import requests +from tests.serve.common import EngineConfig, create_payload_for_config from tests.utils.deployment_graph import ( - Payload, chat_completions_response_handler, completions_response_handler, ) -from tests.utils.managed_process import ManagedProcess +from tests.utils.engine_process import EngineProcess logger = logging.getLogger(__name__) -text_prompt = "Tell me a short joke about AI." - -def create_payload_for_config(config: "TRTLLMConfig") -> Payload: - """Create a payload using the model from the trtllm config""" - return Payload( - payload_chat={ - "model": config.model, - "messages": [ - { - "role": "user", - "content": text_prompt, - } - ], - "max_tokens": 150, - "temperature": 0.1, - }, - payload_completions={ - "model": config.model, - "prompt": text_prompt, - "max_tokens": 150, - "temperature": 0.1, - }, - repeat_count=1, - expected_log=[], - expected_response=["AI"], - ) - - -# TODO: Unify with vllm/sglang tests to reduce code duplication @dataclass -class TRTLLMConfig: +class TRTLLMConfig(EngineConfig): """Configuration for trtllm test scenarios""" - name: str - directory: str - script_name: str - marks: List[Any] - endpoints: List[str] - response_handlers: List[Callable[[Any], str]] - model: str timeout: int = 60 - delayed_start: int = 0 -class TRTLLMProcess(ManagedProcess): +class TRTLLMProcess(EngineProcess): """Simple process manager for trtllm shell scripts""" def __init__(self, config: TRTLLMConfig, request): @@ -97,87 +58,6 @@ def __init__(self, config: TRTLLMConfig, request): log_dir=request.node.name, ) - def _check_models_api(self, response): - """Check if models API is working and returns models""" - try: - if response.status_code != 200: - return False - data = response.json() - return data.get("data") and len(data["data"]) > 0 - except Exception: - return False - - def _check_url(self, url, timeout=30, sleep=2.0): - """Override to use a more reasonable retry interval""" - return super()._check_url(url, timeout, sleep) - - def check_response( - self, payload, response, response_handler, logger=logging.getLogger() - ): - assert response.status_code == 200, "Response Error" - content = response_handler(response) - logger.info(f"Received Content: {content}") - # Check for expected responses - assert content, "Empty response content" - for expected in payload.expected_response: - assert expected in content, f"Expected '{expected}' not found in response" - - def wait_for_ready(self, payload, logger=logging.getLogger()): - url = f"http://localhost:{self.port}/{self.config.endpoints[0]}" - start_time = time.time() - retry_delay = 5 - elapsed = 0.0 - logger.info("Waiting for Deployment Ready") - json_payload = ( - payload.payload_chat - if self.config.endpoints[0] == "v1/chat/completions" - else payload.payload_completions - ) - - while (elapsed := time.time() - start_time) < self.config.timeout: - try: - response = requests.post( - url, - json=json_payload, - timeout=self.config.timeout - elapsed, - ) - except (requests.RequestException, requests.Timeout) as e: - logger.warning(f"Retrying due to Request failed: {e}") - time.sleep(retry_delay) - continue - logger.info(f"Response: {response}") - if response.status_code == 500: - error = response.json().get("error", "") - if "no instances" in error: - logger.warning( - f"Retrying due to no instances available for model '{self.config.model}'" - ) - time.sleep(retry_delay) - continue - if response.status_code == 404: - error = response.json().get("error", "") - if "Model not found" in error: - logger.warning( - f"Retrying due to model not found for model '{self.config.model}'" - ) - time.sleep(retry_delay) - continue - # Process the response - if response.status_code != 200: - pytest.fail( - f"Service returned status code {response.status_code}: {response.text}" - ) - else: - break - else: - pytest.fail( - f"Service did not return a successful response within {self.config.timeout} s" - ) - - self.check_response(payload, response, self.config.response_handlers[0], logger) - - logger.info("Deployment Ready") - # trtllm test configurations trtllm_configs = { @@ -192,7 +72,8 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="deepseek-ai/DeepSeek-R1-Distill-Llama-8B", - delayed_start=60, + delayed_start=0, + timeout=360, ), "disaggregated": TRTLLMConfig( name="disaggregated", @@ -205,7 +86,8 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="deepseek-ai/DeepSeek-R1-Distill-Llama-8B", - delayed_start=60, + delayed_start=0, + timeout=360, ), # TODO: These are sanity tests that the kv router examples launch # and inference without error, but do not do detailed checks on the @@ -221,7 +103,8 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="deepseek-ai/DeepSeek-R1-Distill-Llama-8B", - delayed_start=60, + delayed_start=0, + timeout=360, ), "disaggregated_router": TRTLLMConfig( name="disaggregated_router", @@ -234,7 +117,8 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="deepseek-ai/DeepSeek-R1-Distill-Llama-8B", - delayed_start=60, + delayed_start=0, + timeout=360, ), } @@ -269,8 +153,6 @@ def test_deployment(trtllm_config_test, request, runtime_services): logger.info(f"Script: {config.script_name}") with TRTLLMProcess(config, request) as server_process: - server_process.wait_for_ready(payload, logger) - assert len(config.endpoints) == len(config.response_handlers) for endpoint, response_handler in zip( config.endpoints, config.response_handlers @@ -288,11 +170,7 @@ def test_deployment(trtllm_config_test, request, runtime_services): for _ in range(payload.repeat_count): elapsed = time.time() - start_time - response = requests.post( - url, - json=request_body, - timeout=config.timeout - elapsed, - ) - server_process.check_response( - payload, response, response_handler, logger + response = server_process.send_request( + url, payload=request_body, timeout=config.timeout - elapsed ) + server_process.check_response(payload, response, response_handler) diff --git a/tests/serve/test_vllm.py b/tests/serve/test_vllm.py index 968f07eb0b..31ec74ab4e 100644 --- a/tests/serve/test_vllm.py +++ b/tests/serve/test_vllm.py @@ -5,26 +5,26 @@ import os import time from dataclasses import dataclass -from typing import Any, Callable, List, Optional +from typing import List, Optional import pytest -import requests +from tests.serve.common import EngineConfig +from tests.serve.common import create_payload_for_config as base_create_payload from tests.utils.deployment_graph import ( Payload, chat_completions_response_handler, completions_response_handler, ) -from tests.utils.managed_process import ManagedProcess +from tests.utils.engine_process import EngineProcess logger = logging.getLogger(__name__) -text_prompt = "Tell me a short joke about AI." - def create_payload_for_config(config: "VLLMConfig") -> Payload: """Create a payload using the model from the vLLM config""" if "multimodal" in config.name: + # Special handling for multimodal models return Payload( payload_chat={ "model": config.model, @@ -51,47 +51,18 @@ def create_payload_for_config(config: "VLLMConfig") -> Payload: expected_response=["bus"], ) else: - return Payload( - payload_chat={ - "model": config.model, - "messages": [ - { - "role": "user", - "content": text_prompt, - } - ], - "max_tokens": 150, - "temperature": 0.1, - }, - payload_completions={ - "model": config.model, - "prompt": text_prompt, - "max_tokens": 150, - "temperature": 0.1, - }, - repeat_count=1, - expected_log=[], - expected_response=["AI"], - ) + # Use base implementation for standard text models + return base_create_payload(config) @dataclass -class VLLMConfig: +class VLLMConfig(EngineConfig): """Configuration for vLLM test scenarios""" - name: str - directory: str - script_name: str - marks: List[Any] - endpoints: List[str] - response_handlers: List[Callable[[Any], str]] - model: str - timeout: int = 120 - delayed_start: int = 0 args: Optional[List[str]] = None -class VLLMProcess(ManagedProcess): +class VLLMProcess(EngineProcess): """Simple process manager for vllm shell scripts""" def __init__(self, config: VLLMConfig, request): @@ -122,102 +93,6 @@ def __init__(self, config: VLLMConfig, request): log_dir=request.node.name, ) - def _check_models_api(self, response): - """Check if models API is working and returns models""" - try: - if response.status_code != 200: - return False - data = response.json() - return data.get("data") and len(data["data"]) > 0 - except Exception: - return False - - def _check_url(self, url, timeout=30, sleep=2.0): - """Override to use a more reasonable retry interval""" - return super()._check_url(url, timeout, sleep) - - def check_response( - self, payload, response, response_handler, logger=logging.getLogger() - ): - assert response.status_code == 200, "Response Error" - content = response_handler(response) - logger.info("Received Content: %s", content) - # Check for expected responses - assert content, "Empty response content" - for expected in payload.expected_response: - assert expected in content, "Expected '%s' not found in response" % expected - - def wait_for_ready(self, payload, logger=logging.getLogger()): - url = f"http://localhost:{self.port}/{self.config.endpoints[0]}" - start_time = time.time() - retry_delay = 5 - elapsed = 0.0 - logger.info("Waiting for Deployment Ready") - json_payload = ( - payload.payload_chat - if self.config.endpoints[0] == "v1/chat/completions" - else payload.payload_completions - ) - - while time.time() - start_time < self.config.timeout: - elapsed = time.time() - start_time - try: - response = requests.post( - url, - json=json_payload, - timeout=self.config.timeout - elapsed, - ) - except (requests.RequestException, requests.Timeout) as e: - logger.warning("Retrying due to Request failed: %s", e) - time.sleep(retry_delay) - continue - logger.info("Response%r", response) - if response.status_code == 500: - error = response.json().get("error", "") - if "no instances" in error: - logger.warning("Retrying due to no instances available") - time.sleep(retry_delay) - continue - elif ( - "multimodal" in self.config.name - and "Failed to fold chat completions stream" in error - ): - logger.warning("Retrying due to endpoint not ready for multimodal") - time.sleep(retry_delay) - continue - if response.status_code == 404: - error = response.json().get("error", "") - if "Model not found" in error: - logger.warning("Retrying due to model not found") - time.sleep(retry_delay) - continue - # Process the response - if response.status_code != 200: - logger.error( - "Service returned status code %s: %s", - response.status_code, - response.text, - ) - pytest.fail( - "Service returned status code %s: %s" - % (response.status_code, response.text) - ) - else: - break - else: - logger.error( - "Service did not return a successful response within %s s", - self.config.timeout, - ) - pytest.fail( - "Service did not return a successful response within %s s" - % self.config.timeout - ) - - self.check_response(payload, response, self.config.response_handlers[0], logger) - - logger.info("Deployment Ready") - # vLLM test configurations vllm_configs = { @@ -232,7 +107,8 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="Qwen/Qwen3-0.6B", - delayed_start=45, + delayed_start=0, + timeout=360, ), "agg-router": VLLMConfig( name="agg-router", @@ -245,7 +121,8 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="Qwen/Qwen3-0.6B", - delayed_start=45, + delayed_start=0, + timeout=360, ), "disaggregated": VLLMConfig( name="disaggregated", @@ -258,7 +135,8 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="Qwen/Qwen3-0.6B", - delayed_start=45, + delayed_start=0, + timeout=360, ), "deepep": VLLMConfig( name="deepep", @@ -275,7 +153,7 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): completions_response_handler, ], model="deepseek-ai/DeepSeek-V2-Lite", - delayed_start=45, + delayed_start=0, args=[ "--model", "deepseek-ai/DeepSeek-V2-Lite", @@ -286,7 +164,7 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): "--gpus-per-node", "2", ], - timeout=500, + timeout=560, ), "multimodal_agg": VLLMConfig( name="multimodal_agg", @@ -298,8 +176,9 @@ def wait_for_ready(self, payload, logger=logging.getLogger()): chat_completions_response_handler, ], model="llava-hf/llava-1.5-7b-hf", - delayed_start=45, + delayed_start=0, args=["--model", "llava-hf/llava-1.5-7b-hf"], + timeout=360, ), # TODO: Enable this test case when we have 4 GPUs runners. # "multimodal_disagg": VLLMConfig( @@ -348,8 +227,6 @@ def test_serve_deployment(vllm_config_test, request, runtime_services): logger.info("Script: %s", config.script_name) with VLLMProcess(config, request) as server_process: - server_process.wait_for_ready(payload, logger) - for endpoint, response_handler in zip( config.endpoints, config.response_handlers ): @@ -366,11 +243,7 @@ def test_serve_deployment(vllm_config_test, request, runtime_services): for _ in range(payload.repeat_count): elapsed = time.time() - start_time - response = requests.post( - url, - json=request_body, - timeout=config.timeout - elapsed, - ) - server_process.check_response( - payload, response, response_handler, logger + response = server_process.send_request( + url, payload=request_body, timeout=config.timeout - elapsed ) + server_process.check_response(payload, response, response_handler) diff --git a/tests/utils/deployment_graph.py b/tests/utils/deployment_graph.py index bc83aa7914..2da6ef9244 100644 --- a/tests/utils/deployment_graph.py +++ b/tests/utils/deployment_graph.py @@ -40,8 +40,41 @@ def chat_completions_response_handler(response): assert "choices" in result, "Missing 'choices' in response" assert len(result["choices"]) > 0, "Empty choices in response" assert "message" in result["choices"][0], "Missing 'message' in first choice" - assert "content" in result["choices"][0]["message"], "Missing 'content' in message" - return result["choices"][0]["message"]["content"] + + message = result["choices"][0]["message"] + + # Check for content in all possible fields where parsers might put output: + # 1. content - standard message content + # 2. reasoning_content - for models with reasoning parsers + # 3. refusal - when the model refuses to answer + # 4. tool_calls - for function/tool calling responses + + content = message.get("content", "") + reasoning_content = message.get("reasoning_content", "") + refusal = message.get("refusal", "") + + # Check for tool calls + tool_calls = message.get("tool_calls", []) + tool_content = "" + if tool_calls: + # Extract content from tool calls + tool_content = ", ".join( + call.get("function", {}).get("arguments", "") + for call in tool_calls + if call.get("function", {}).get("arguments") + ) + + # Return the first non-empty field in priority order + for field_content in [content, reasoning_content, refusal, tool_content]: + if field_content: + return field_content + + # If all fields are empty, provide a detailed error + raise ValueError( + "All possible content fields are empty in message. " + f"Checked: content={repr(content)}, reasoning_content={repr(reasoning_content)}, " + f"refusal={repr(refusal)}, tool_calls={tool_calls}" + ) def completions_response_handler(response): diff --git a/tests/utils/engine_process.py b/tests/utils/engine_process.py new file mode 100644 index 0000000000..7a5c492946 --- /dev/null +++ b/tests/utils/engine_process.py @@ -0,0 +1,164 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import logging +import time +from typing import Any, Callable, Dict + +import requests + +from tests.utils.managed_process import ManagedProcess + +logger = logging.getLogger(__name__) + + +class EngineResponseError(Exception): + """Custom exception for engine response errors""" + + pass + + +class EngineProcess(ManagedProcess): + """Base class for LLM engine processes (vLLM, TRT-LLM, etc.)""" + + def _check_models_api(self, response): + """Check if models API is working and returns models""" + try: + if response.status_code != 200: + return False + data = response.json() + return data.get("data") and len(data["data"]) > 0 + except Exception: + return False + + def send_request( + self, url: str, payload: Dict[str, Any], timeout: float = 30.0 + ) -> requests.Response: + """ + Send a POST request to the engine with detailed logging. + + Args: + url: The endpoint URL + payload: The request payload + timeout: Request timeout in seconds + + Returns: + The response object + + Raises: + requests.RequestException: If the request fails + """ + + # Log the request as a curl command for easy reproduction + payload_json = json.dumps(payload, indent=2) + curl_command = f'curl -X POST "{url}" \\\n -H "Content-Type: application/json" \\\n -d \'{payload_json}\'' + logger.info("Sending request (curl equivalent):\n%s", curl_command) + + start_time = time.time() + try: + response = requests.post(url, json=payload, timeout=timeout) + elapsed = time.time() - start_time + + # Log response details + logger.info( + "Received response: status=%d, elapsed=%.2fs", + response.status_code, + elapsed, + ) + + logger.debug("Response headers: %s", dict(response.headers)) + + # Try to log response body (truncated if too long) + try: + if response.headers.get("content-type", "").startswith( + "application/json" + ): + response_data = response.json() + response_str = json.dumps(response_data, indent=2) + if len(response_str) > 1000: + response_str = response_str[:1000] + "... (truncated)" + logger.debug("Response body: %s", response_str) + else: + response_text = response.text + if len(response_text) > 1000: + response_text = response_text[:1000] + "... (truncated)" + logger.debug("Response body: %s", response_text) + except Exception as e: + logger.debug("Could not parse response body: %s", e) + + return response + + except requests.exceptions.Timeout: + logger.error("Request timed out after %.2f seconds", timeout) + raise + except requests.exceptions.ConnectionError as e: + logger.error("Connection error: %s", e) + raise + except requests.exceptions.RequestException as e: + logger.error("Request failed: %s", e) + raise + + def check_response( + self, + payload: Any, + response: requests.Response, + response_handler: Callable[[Any], str], + ) -> None: + """ + Check if the response is valid and contains expected content. + + Args: + payload: The original payload (should have expected_response attribute) + response: The response object + response_handler: Function to extract content from response + + Raises: + EngineResponseError: If the response is invalid or missing expected content + """ + + if response.status_code != 200: + logger.error( + "Response returned non-200 status code: %d", response.status_code + ) + + error_msg = f"Response returned non-200 status code: {response.status_code}" + try: + error_data = response.json() + if "error" in error_data: + error_msg += f"\nError details: {error_data['error']}" + logger.error( + "Response error details: %s", json.dumps(error_data, indent=2) + ) + except Exception: + logger.error("Response text: %s", response.text[:500]) + + raise EngineResponseError(error_msg) + + # Extract content using the handler + try: + content = response_handler(response) + logger.info( + "Extracted content: \n%s", + content[:200] + "..." if len(content) > 200 else content, + ) + except Exception as e: + raise EngineResponseError(f"Failed to extract content from response: {e}") + + if not content: + raise EngineResponseError("Response contained empty content") + + if hasattr(payload, "expected_response") and payload.expected_response: + missing_expected = [] + for expected in payload.expected_response: + if expected not in content: + missing_expected.append(expected) + + if missing_expected: + raise EngineResponseError( + f"Expected content not found in response. Missing: {missing_expected}" + ) + else: + logger.info( + f"SUCCESS: All expected content ({payload.expected_response}) found in response" + ) diff --git a/tests/utils/managed_process.py b/tests/utils/managed_process.py index a62110be40..af82a994c8 100644 --- a/tests/utils/managed_process.py +++ b/tests/utils/managed_process.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging import os import shutil @@ -204,6 +205,39 @@ def _remove_directory(self, path: str) -> None: except (OSError, IOError) as e: self._logger.warning("Warning: Failed to remove directory %s: %s", path, e) + def _log_tail_on_error(self, lines=20): + """Print the last few lines of the log file when process dies.""" + if self._log_path and os.path.exists(self._log_path): + try: + with open(self._log_path, "r") as f: + log_lines = f.readlines() + if log_lines: + self._logger.error( + "=== Last %d lines from %s ===", + min(lines, len(log_lines)), + self._log_path, + ) + for line in log_lines[-lines:]: + self._logger.error(line.rstrip()) + self._logger.error("=== End of log tail ===") + except Exception as e: + self._logger.warning("Could not read log file: %s", e) + + def _check_process_alive(self, context=""): + """Check if the main process is still alive. Raises RuntimeError if dead.""" + if self.proc and self.proc.poll() is not None: + returncode = self.proc.returncode + self._logger.error( + "Main server process died with exit code %d%s", + returncode, + f" {context}" if context else "", + ) + # Try to get last few lines from log for debugging + self._log_tail_on_error() + raise RuntimeError( + f"Main server process exited with code {returncode}{f' {context}' if context else ''}" + ) + def _check_ports(self, timeout): elapsed = 0.0 for port in self.health_check_ports: @@ -216,6 +250,9 @@ def _check_port(self, port, timeout=30, sleep=0.1): self._logger.info("Checking Port: %s", port) elapsed = 0.0 while elapsed < timeout: + # Check if the main process is still alive + self._check_process_alive(f"while waiting for port {port}") + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: if s.connect_ex(("localhost", port)) == 0: self._logger.info("SUCCESS: Check Port: %s", port) @@ -231,7 +268,7 @@ def _check_urls(self, timeout): elapsed += self._check_url(url, timeout - elapsed) return elapsed - def _check_url(self, url, timeout=30, sleep=0.1): + def _check_url(self, url, timeout=30, sleep=1, log_interval=10): if isinstance(url, tuple): response_check = url[1] url = url[0] @@ -240,19 +277,71 @@ def _check_url(self, url, timeout=30, sleep=0.1): start_time = time.time() self._logger.info("Checking URL %s", url) elapsed = 0.0 + attempt = 0 + last_log_time = 0.0 + while elapsed < timeout: + self._check_process_alive("while waiting for health check") + + attempt += 1 + check_failed = False + failure_reason = None + try: response = requests.get(url, timeout=timeout - elapsed) if response.status_code == 200: if response_check is None or response_check(response): - self._logger.info("SUCCESS: Check URL: %s", url) + # Try to format JSON response nicely, otherwise show raw text + try: + response_data = response.json() + response_str = json.dumps(response_data, indent=2) + self._logger.info( + "SUCCESS: Check URL: %s (attempt=%d, elapsed=%.1fs)\nResponse:\n%s", + url, + attempt, + elapsed, + response_str, + ) + except (json.JSONDecodeError, Exception): + # If not JSON or any error, show raw text (truncated if too long) + response_text = response.text + if len(response_text) > 500: + response_text = response_text[:500] + "... (truncated)" + self._logger.info( + "SUCCESS: Check URL: %s (attempt=%d, elapsed=%.1fs)\nResponse: %s", + url, + attempt, + elapsed, + response_text, + ) return time.time() - start_time + else: + check_failed = True + failure_reason = "custom check failed" + else: + check_failed = True + failure_reason = f"status code {response.status_code}" except requests.RequestException as e: - self._logger.warning("URL check failed: %s", e) + check_failed = True + failure_reason = f"request exception: {e}" + + # Log progress every log_interval seconds for any failure + if check_failed and elapsed - last_log_time >= log_interval: + self._logger.info( + "Still waiting for URL %s (%s) (attempt=%d, elapsed=%.1fs)", + url, + failure_reason, + attempt, + elapsed, + ) + last_log_time = elapsed + time.sleep(sleep) elapsed = time.time() - start_time - self._logger.error("FAILED: Check URL: %s", url) + self._logger.error( + "FAILED: Check URL: %s (attempts=%d, elapsed=%.1fs)", url, attempt, elapsed + ) raise RuntimeError("FAILED: Check URL: %s" % url) def _terminate_existing(self): From d18b01c181c359a9d3d61f26b54f141b7abf2d6a Mon Sep 17 00:00:00 2001 From: Hongkuan Zhou Date: Mon, 25 Aug 2025 09:08:02 -0700 Subject: [PATCH 20/82] fix: correct planner test example after tokenizer fix (#2674) Signed-off-by: Jason Zhou --- .../planner/src/dynamo/planner/planner_sla.py | 2 +- .../{argparse.py => planner_argparse.py} | 0 components/planner/test/planner_sla_dryrun.py | 2 +- tests/planner/README.md | 37 +++++++++--------- tests/planner/figures/dryrun_plot.png | Bin 740824 -> 703293 bytes .../raw_data.npz | Bin 2618 -> 2618 bytes .../raw_data.npz | Bin 1212 -> 1212 bytes 7 files changed, 21 insertions(+), 20 deletions(-) rename components/planner/src/dynamo/planner/utils/{argparse.py => planner_argparse.py} (100%) diff --git a/components/planner/src/dynamo/planner/planner_sla.py b/components/planner/src/dynamo/planner/planner_sla.py index 030d4cadd4..c0623b5d44 100644 --- a/components/planner/src/dynamo/planner/planner_sla.py +++ b/components/planner/src/dynamo/planner/planner_sla.py @@ -19,7 +19,7 @@ from pydantic import BaseModel from dynamo.planner.defaults import SLAPlannerDefaults -from dynamo.planner.utils.argparse import create_sla_planner_parser +from dynamo.planner.utils.planner_argparse import create_sla_planner_parser from dynamo.planner.utils.planner_core import start_sla_planner from dynamo.runtime import DistributedRuntime, dynamo_worker diff --git a/components/planner/src/dynamo/planner/utils/argparse.py b/components/planner/src/dynamo/planner/utils/planner_argparse.py similarity index 100% rename from components/planner/src/dynamo/planner/utils/argparse.py rename to components/planner/src/dynamo/planner/utils/planner_argparse.py diff --git a/components/planner/test/planner_sla_dryrun.py b/components/planner/test/planner_sla_dryrun.py index a27ce0d4ac..9e4824cbfb 100644 --- a/components/planner/test/planner_sla_dryrun.py +++ b/components/planner/test/planner_sla_dryrun.py @@ -15,7 +15,7 @@ import logging -from dynamo.planner.utils.argparse import create_sla_planner_parser +from dynamo.planner.utils.planner_argparse import create_sla_planner_parser from dynamo.planner.utils.planner_core import Planner logger = logging.getLogger(__name__) diff --git a/tests/planner/README.md b/tests/planner/README.md index c027fc601b..cc6d4b3c66 100644 --- a/tests/planner/README.md +++ b/tests/planner/README.md @@ -48,42 +48,43 @@ python components/planner/src/dynamo/planner/utils/perf_interpolation.py \ --ttft 0.1 \ --itl 0.01 -> ISL=3000, OSL=300 -> TTFT=0.1s, ITL=0.01s -> Using profile results from tests/planner/profiling_results/H200_TP1P_TP1D/ -> -> Interpolating prefill performance ... -> Estimated TTFT=0.027s <= target TTFT=0.100s. Requests can queue 0.073s maximally while meeting TTFT SLA. -> Estimated throughput: 110893.48 tokens/s/gpu. Request rate at 36.96 requests/s will saturate one GPU. +# output: +ISL=3000, OSL=300 +TTFT=0.1s, ITL=0.01s +Using profile results from tests/planner/profiling_results/H200_TP1P_TP1D/ + +Interpolating prefill performance ... + Estimated TTFT=0.060s <= target TTFT=0.100s. Requests can queue 0.040s maximally while meeting TTFT SLA. + Estimated throughput: 49481.09 tokens/s/gpu. Request rate at 16.49 requests/s will saturate one GPU. Interpolating decode performance ... -> Average context length: isl + osl/2 = 3150. -> Estimated ITL=0.0098s <= target ITL=0.0100s at 36.36% active kv usage. -> Estimated throughput: 10009.88 token/s/gpu. Request rate at 33.37 requests/s will saturate one GPU. + Average context length: isl + osl/2 = 3150. + Estimated ITL=0.0097s <= target ITL=0.0100s at 16.16% active kv usage. + Estimated throughput: 4555.68 token/s/gpu. Request rate at 15.19 requests/s will saturate one GPU. ``` ## Generating Load Dataset We provide a tool to generate load dataset with varying request rate. More details can be found in [sin_load_generator](../../benchmarks/sin_load_generator/README.md). -From previous interpolator testing, ISL 3000 and OSL 300 can handle ~30 request/s/gpu for both prefill and decode. -To test planner's performance for different request rates, we can generate a load dataset with request rate varying between 20 to 80 request/s. +From previous interpolator testing, ISL 3000 and OSL 300 can handle ~15 request/s/gpu for both prefill and decode. +To test planner's performance for different request rates, we can generate a load dataset with request rate varying between 12 to 36 request/s. For TP1 H200 engine, planner should scale between 1P1D and 3P3D. ```bash python benchmarks/sin_load_generator/sin_synth.py \ --time-duration 1800 \ - --request-rate-min 20 \ - --request-rate-max 80 \ + --request-rate-min 12 \ + --request-rate-max 36 \ --request-rate-period 600 \ --isl1 3000 \ --osl1 300 \ --isl2 3000 \ --osl2 300 \ - --output-file rr-20-80_i3000o300.jsonl + --output-file rr-12-36_i3000o300.jsonl ``` -The dataset starts at 20 requests/s, increases to 80 requests/s at t=300s, decreases back to 20 requests/s at t=600s, and repeats. +The dataset starts at 12 requests/s, increases to 36 requests/s at t=300s, decreases back to 12 requests/s at t=600s, and repeats. The total duration is 30 minutes or 1800 seconds. ## Planner Dry Run @@ -103,7 +104,7 @@ python components/planner/test/planner_sla_dryrun.py \ --output-plot ``` -For example, to dry run SLA planner for the previous FP8 8B on H200 using the generated `rr-20-80_i3000o300.jsonl` dataset, +For example, to dry run SLA planner for the previous FP8 8B on H200 using the generated `rr-12-36_i3000o300.jsonl` dataset, ```bash python components/planner/test/planner_sla_dryrun.py \ @@ -111,7 +112,7 @@ python components/planner/test/planner_sla_dryrun.py \ --itl 0.01 \ --adjustment-interval 60 \ --profile-results-dir tests/planner/profiling_results/H200_TP1P_TP1D/ \ - --dataset rr-20-80_i3000o300.jsonl \ + --dataset rr-12-36_i3000o300.jsonl \ --start-num-p 1 \ --start-num-d 1 \ --output-plot dryrun_plot.png diff --git a/tests/planner/figures/dryrun_plot.png b/tests/planner/figures/dryrun_plot.png index a3f6368b3b51de09796179e1d53ba727f8a8be31..bfde37844ebd1c62b6a41c36155393a965b3b44d 100644 GIT binary patch literal 703293 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 { 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 z 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

heVKt(<7>q(JJS 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

a9Jviqp+*PkMH5KLg662dOn#pFkj{_`Mj`OHP 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�-UeN<4OhP#RWa@1?=LG>P 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

B3IK&W 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

s{ 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

Csm z=-Tc21aFOkHITy%wl&3HW*MG1CU0}5>=1b6LY0$}%^eZBWd&VO`2hQY=dc7;7 zlOPS`ki;u4ldHo}Ic=c$;$rdZ2w#IV?t5~HJ*bo;AQ(&@;Jfk6HkjU~^n=Q|#d^>D zCgfH4BW*i-Bd9B24kH)nv(x~y7TGrWP4B*0TpGJ_)^S)YXpBlyl_hsd19S#dZ~hxd zu$3FoaIaH0NL}Awn|)eUrJTVmBViZQY(IO0pG_-RgkW-0;1VOMx>k+&E}wF^De0}@ zSme`hlGRwY=(`X`NB2ELRC&*>v1p@lLYz_M1#UTHPU~v@hf_;9Q^A_j4Rn6N5JkWd zTl$d`*B7$B8$ZzYL0p3AY`Zt_hLrfgxi2YR;( z({bE$iLcJy5De~qJhfES&}^|VJQDl|4hd!eu3nW-JWk&BNE*`vg%sQ zQoFYu-w}Cty^BD#mpxi1j%S{$F?KrbLK60OpusezFVndg`#4K=z5D9sznae}bhq4o z4=y#T%Pzyu5=plV8OjN%dgjdRVh@mjYQ%icqPRoOd5Aa%Pu z$$701l-xIG>`LcOtjCu72MDWniy$0&cx5p+Kwd>PedgX@_(8v$)w;R6Gn)`}=2wSu zKmaa3eHgq1H95H_GUc|CnS!1;Pt419b$PJ?_jhRaY13txGe{0y&Lxk) z^6UNez2s2PlFsql!2ZzxJtw~#ISAeT`PrAS{-?*)uf8)(VzU}wK&?ZqXa1}IFT zwjGioM=g6xJz&njg8@^j0hrNAdAtR3eeB2c=VQ#A$o$?8aB8Zm3%u7g0pSWdfU)^H z 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 Efpr*0OaK4? delta 722 zcmV;@0xkWz6uK0!Bm@KqKo!N4C}Ih-upg4L^i9l zPX|RnRBrFBxj{xiK-=r9dy5f34m%h3y%8CIK>n*pVI_vK(dlWig1O>Kzl}hlRg*FK#xf*L)O>PK)T50AYK&k^{PglspKwOHhVGYv6Kv`G3 zDQ@$~Kv^7#T+J}cK)H`g(&-|{K##5p0MA8iKm*&QvvqHi+PXyg*ofG8Ly8z(CU-&Ek{hw?HrlDU7DH#6W)!6sUedY(QPE7V{=mra*BA)sqie zu|NPf)wM&UNw`2OKqW~2u)RS3391S14!=M}UH%`}AZ$Qz^)wY^C!#+)vG`$ z1xDo_x3fSub@YnTV!1$5sPE;BG`&FV6<_j6tG%-%2wDLI2tXCZlY$8|1QZhwyOX&I EfhdGG7ytkO diff --git a/tests/planner/profiling_results/H200_TP1P_TP1D/selected_prefill_interpolation/raw_data.npz b/tests/planner/profiling_results/H200_TP1P_TP1D/selected_prefill_interpolation/raw_data.npz index 484f9c0115c3334536dd7819ddb0c824f691da44..13ed27986130b687d55a5dc5131340da63b754bb 100644 GIT binary patch delta 336 zcmV-W0k8hN3A_ogR{;bM1@RJ-TLG|tn~>HRbcQoPN_axAKR-i25gNh9_4`Obgwui- zeVb1}Z5Rc?9G7uZ15s!| z1jl9ER03^4K=yj~M+|O2&WwIETxW1VQw5`vdV+F5LpnT!ag}pG-%pK~ZXtFXKw#&l z&A4}WKu}8q1T6pn0000003iVWnq5(or2@ErnJKe}l>xLsnQ|>fT2|aZP;IN`2V>s+;9Nu@qjRLha^2w4#}~@Z;t{vgxwHdiLc&E?#Fk(pTm{X41{Vmn`K8 iKmumPp;1cZK$GPIO#}}G@e-3Z1Udx%nq5(obOcCKJeYX^ delta 336 zcmV-W0k8hN3A_ogR{;dS7em^UTLG|tEs+b(;Fl~w7ny)xBjGbZm;Jiaol`bIC=ALp zniM-grAz;kRBAXtj^Z43bd)?mPaYO(O}{%p8)j%c5ez>-j*;vUY9c&9>Lc)np9eoc z3~{WB+g?IIr_DGGQCC1frklhduqH!5Q_Mlo(Skrg1}fv!qx;qqFy|aX(a|hCJqrYhV7v@KD`E^mC^!F-1>O-skCKtL zRWlMn){&vAjt>z*U2I8aG87d-0V}4FNt+WvCDS;P6`vSE(K;5CgSi<&@ Date: Mon, 25 Aug 2025 09:28:25 -0700 Subject: [PATCH 21/82] feat: add initial batch of KVBM metrics on match, offload and onboard (#2673) Signed-off-by: Jason Zhou --- .../grafana-kvbm-dashboard.json | 933 ++++++++++++++---- .../block_manager/vllm/connector/leader.rs | 7 +- .../vllm/connector/leader/recorder.rs | 3 +- .../vllm/connector/leader/slot.rs | 28 +- .../block_manager/vllm/connector/worker.rs | 2 +- lib/llm/src/block_manager/metrics_kvbm.rs | 50 + 6 files changed, 809 insertions(+), 214 deletions(-) diff --git a/deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json b/deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json index 56ed93c779..c5717a3a00 100644 --- a/deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json +++ b/deploy/metrics/grafana_dashboards/grafana-kvbm-dashboard.json @@ -1,234 +1,753 @@ { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "All KVBM related metrics", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 4, - "links": [], - "panels": [ + "annotations": { + "list": [ { + "builtIn": 1, "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" + "type": "grafana", + "uid": "-- Grafana --" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "All KVBM related metrics", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 6, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 7, + "panels": [], + "title": "General", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" }, - "thresholdsStyle": { - "mode": "off" + { + "color": "red", + "value": 80 } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_matched_tokens{dynamo_namespace=\"kvbm_connector_leader\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Matched Tokens", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 5, + "panels": [], + "title": "Offload", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_offload_requests{dynamo_namespace=\"kvbm_connector_leader\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Offload Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] } }, - "pluginVersion": "12.0.1", - "targets": [ - { - "disableTextWrap": false, - "editorMode": "builder", - "expr": "dynamo_component_save_kv_layer_requests{dynamo_namespace=\"kvbm_connector_worker\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "KVBM Worker: save kv layer requests", - "type": "timeseries" + "overrides": [] }, - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 10 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_offload_blocks_d2h{dynamo_namespace=\"kvbm_connector_leader\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Offload Blocks", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" }, - "thresholdsStyle": { - "mode": "off" + { + "color": "red", + "value": 80 } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_save_kv_layer_requests{dynamo_namespace=\"kvbm_connector_worker\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Save KV Layer Requests", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 6, + "panels": [], + "title": "Onboard", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 8 + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_onboard_requests{dynamo_namespace=\"kvbm_connector_leader\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Onboard Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] } }, - "pluginVersion": "12.0.1", - "targets": [ - { - "disableTextWrap": false, - "editorMode": "builder", - "expr": "dynamo_component_offload_requests{dynamo_namespace=\"kvbm_connector_leader\"}", - "fullMetaSearch": false, - "includeNullMetadata": true, - "legendFormat": "__auto", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "KVBM Leader: offload requests", - "type": "timeseries" - } - ], - "preload": false, - "refresh": "auto", - "schemaVersion": 41, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-15m", - "to": "now" + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_onboard_blocks_h2d{dynamo_namespace=\"kvbm_connector_leader\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Onboard Blocks - Host to Device", + "type": "timeseries" }, - "timepicker": {}, - "timezone": "browser", - "title": "KVBM Dashboard", - "uid": "3f679257-70a5-402c-92b4-05382337b548", - "version": 7 - } + { + "datasource": { + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "dynamo_component_onboard_blocks_d2d{dynamo_namespace=\"kvbm_connector_leader\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Onboard Blocks - Disk to Host", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "auto", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "KVBM Dashboard", + "uid": "3f679257-70a5-402c-92b4-05382337b548", + "version": 5 +} \ No newline at end of file diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs index d6b787de74..3449847016 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader.rs @@ -80,6 +80,7 @@ pub struct KvConnectorLeader { inflight_requests: HashSet, onboarding_slots: HashSet, iteration_counter: u64, + kvbm_metrics: KvbmMetrics, } impl KvConnectorLeader { @@ -114,12 +115,13 @@ impl KvConnectorLeader { block_manager.clone(), leader, drt.clone(), - kvbm_metrics, + kvbm_metrics.clone(), ), block_size, inflight_requests: HashSet::new(), onboarding_slots: HashSet::new(), iteration_counter: 0, + kvbm_metrics, } } } @@ -188,6 +190,9 @@ impl Leader for KvConnectorLeader { "scheduling onboarding for {} external tokens", num_external_tokens ); + self.kvbm_metrics + .matched_tokens + .inc_by(num_external_tokens as u64); Ok((num_external_tokens, true)) } else { Ok((0, false)) diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs index 6506851c2a..29f62c1ceb 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/recorder.rs @@ -124,12 +124,13 @@ impl KvConnectorLeaderRecorder { block_manager.clone(), leader, drt.clone(), - kvbm_metrics, + kvbm_metrics.clone(), ), block_size, inflight_requests: HashSet::new(), onboarding_slots: HashSet::new(), iteration_counter: 0, + kvbm_metrics, }; let (unbounded_tx, unbounded_rx) = mpsc::unbounded_channel(); diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs index 74c729e11a..6ef91aefb6 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/leader/slot.rs @@ -197,7 +197,7 @@ impl ConnectorSlotManager { let xfer_engine_task = CriticalTaskExecutionHandle::new_with_runtime( |cancellation_token| async move { xfer_engine - .execute(cancellation_token, drt_for_task, kvbm_metrics.clone()) + .execute(cancellation_token, drt_for_task, kvbm_metrics) .await }, primary_token, @@ -1042,6 +1042,9 @@ impl LocalTransferEngine { let leader_offload = Arc::clone(&self.leader); let leader_onboard = Arc::clone(&self.leader); + let kvbm_metrics_onboard = kvbm_metrics.clone(); + let kvbm_metrics_offload = kvbm_metrics.clone(); + let onboard_task = CriticalTaskExecutionHandle::new_with_runtime( |cancellation_token_onboard| async move { while let Some(req) = onboard_rx.recv().await { @@ -1049,7 +1052,10 @@ impl LocalTransferEngine { tracing::debug!("LocalOnboardTask: received cancellation signal"); break; } - if let Err(e) = process_onboard_request(req, &leader_onboard).await { + if let Err(e) = + process_onboard_request(req, &leader_onboard, kvbm_metrics_onboard.clone()) + .await + { tracing::error!("LocalOnboardTask: error processing request: {:?}", e); } } @@ -1071,7 +1077,7 @@ impl LocalTransferEngine { req, &block_manager_offload, &leader_offload, - kvbm_metrics.clone(), + kvbm_metrics_offload.clone(), ) .await { @@ -1145,6 +1151,9 @@ async fn process_offload_request( kvbm_metrics: KvbmMetrics, ) -> anyhow::Result<()> { kvbm_metrics.offload_requests.inc(); + kvbm_metrics + .offload_blocks_d2h + .inc_by(offload_req.block_ids.len() as u64); let request_id = &offload_req.request_id; let operation_id = &offload_req.operation_id; @@ -1154,7 +1163,6 @@ async fn process_offload_request( offload_req.block_ids.len() ); - // TODO: Implement actual offload logic // 1. Acquire mutable host blocks let host_blocks = block_manager .host() @@ -1250,7 +1258,19 @@ async fn process_offload_request( async fn process_onboard_request( onboard_req: LocalOnboardRequest, leader: &Arc, + kvbm_metrics: KvbmMetrics, ) -> anyhow::Result<()> { + kvbm_metrics.onboard_requests.inc(); + if onboard_req.src_blocks.storage_pool() == BlockTransferPool::Host { + kvbm_metrics + .onboard_blocks_h2d + .inc_by(onboard_req.src_blocks.len() as u64); + } else if onboard_req.src_blocks.storage_pool() == BlockTransferPool::Disk { + kvbm_metrics + .onboard_blocks_d2d + .inc_by(onboard_req.src_blocks.len() as u64); + } + let request_id = &onboard_req.request_id; let operation_id = &onboard_req.operation_id; diff --git a/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs b/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs index eccb80b473..ba25fad0f9 100644 --- a/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs +++ b/lib/bindings/python/rust/llm/block_manager/vllm/connector/worker.rs @@ -265,7 +265,6 @@ impl Worker for KvConnectorWorker { /// Trigger layer-wise completion signals. /// Trigger block-wise completion signals afer last layer. fn save_kv_layer(&mut self, _layer_name: String) -> anyhow::Result<()> { - self.kvbm_metrics.save_kv_layer_requests.inc(); self.layers_complete += 1; if self.layers_complete == self.kv_cache_layers.len() { let offloading_operations = std::mem::take(&mut self.offloading_operations); @@ -278,6 +277,7 @@ impl Worker for KvConnectorWorker { self.connector.enqueue_request(operation); } } + self.kvbm_metrics.save_kv_layer_requests.inc(); Ok(()) } diff --git a/lib/llm/src/block_manager/metrics_kvbm.rs b/lib/llm/src/block_manager/metrics_kvbm.rs index 0b8e58d3f2..07fa9f1be0 100644 --- a/lib/llm/src/block_manager/metrics_kvbm.rs +++ b/lib/llm/src/block_manager/metrics_kvbm.rs @@ -6,8 +6,26 @@ use prometheus::IntCounter; #[derive(Clone, Debug)] pub struct KvbmMetrics { + // number of offload requests pub offload_requests: IntCounter, + + // number of blocks offloaded from device to host + pub offload_blocks_d2h: IntCounter, + + // number of onboard requests + pub onboard_requests: IntCounter, + + // number of blocks onboarded from host to device + pub onboard_blocks_h2d: IntCounter, + + // number of blocks onboarded from disk to device + pub onboard_blocks_d2d: IntCounter, + + // number of save kv layer requests pub save_kv_layer_requests: IntCounter, + + // number of matched tokens from KVBM + pub matched_tokens: IntCounter, } impl KvbmMetrics { @@ -15,6 +33,30 @@ impl KvbmMetrics { let offload_requests = mr .create_intcounter("offload_requests", "The number of offload requests", &[]) .unwrap(); + let offload_blocks_d2h = mr + .create_intcounter( + "offload_blocks_d2h", + "The number of offload blocks from device to host", + &[], + ) + .unwrap(); + let onboard_requests = mr + .create_intcounter("onboard_requests", "The number of onboard requests", &[]) + .unwrap(); + let onboard_blocks_h2d = mr + .create_intcounter( + "onboard_blocks_h2d", + "The number of onboard blocks from host to device", + &[], + ) + .unwrap(); + let onboard_blocks_d2d = mr + .create_intcounter( + "onboard_blocks_d2d", + "The number of onboard blocks from disk to device", + &[], + ) + .unwrap(); let save_kv_layer_requests = mr .create_intcounter( "save_kv_layer_requests", @@ -22,9 +64,17 @@ impl KvbmMetrics { &[], ) .unwrap(); + let matched_tokens = mr + .create_intcounter("matched_tokens", "The number of matched tokens", &[]) + .unwrap(); Self { offload_requests, + offload_blocks_d2h, + onboard_requests, + onboard_blocks_h2d, + onboard_blocks_d2d, save_kv_layer_requests, + matched_tokens, } } } From 878afde0f2e75b1b6a5e21604d9000a47e7e7cae Mon Sep 17 00:00:00 2001 From: nachiketb-nvidia Date: Mon, 25 Aug 2025 10:13:38 -0700 Subject: [PATCH 22/82] feat: add gpt oss reasoning parser through harmony (#2656) - couple of refactors - added a new dependency, openai-harmony - implemented the gpt oss parser Signed-off-by: Jason Zhou --- Cargo.lock | 339 +++++++++- deny.toml | 3 +- lib/bindings/python/Cargo.lock | 630 +++++++++++++++++- lib/llm/src/engines.rs | 4 +- .../openai/chat_completions/delta.rs | 46 +- lib/llm/tests/http-service.rs | 2 +- lib/parsers/Cargo.toml | 4 +- lib/parsers/src/reasoning/base_parser.rs | 103 +-- .../src/reasoning/deepseek_r1_parser.rs | 13 +- lib/parsers/src/reasoning/gpt_oss_parser.rs | 249 +++++++ lib/parsers/src/reasoning/mod.rs | 42 +- 11 files changed, 1348 insertions(+), 87 deletions(-) create mode 100644 lib/parsers/src/reasoning/gpt_oss_parser.rs diff --git a/Cargo.lock b/Cargo.lock index 5d0423bbb9..465c91c5ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,17 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -341,6 +352,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 7.1.3", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.13.3" @@ -693,6 +727,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "blake3" version = "1.8.2" @@ -753,9 +793,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", + "regex-automata 0.4.9", "serde", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.19.0" @@ -1991,6 +2038,8 @@ version = "0.4.1" dependencies = [ "anyhow", "dynamo-async-openai", + "lazy_static", + "openai-harmony", "regex", "serde", "serde_json", @@ -2353,6 +2402,17 @@ dependencies = [ "regex", ] +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set 0.5.3", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + [[package]] name = "fancy-regex" version = "0.14.0" @@ -3130,6 +3190,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hf-hub" version = "0.4.3" @@ -3548,6 +3614,9 @@ dependencies = [ "num-traits", "png", "qoi", + "ravif", + "rayon", + "rgb", "tiff", "zune-core", "zune-jpeg", @@ -3563,6 +3632,12 @@ dependencies = [ "quick-error 2.0.1", ] +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indexmap" version = "1.9.3" @@ -3571,6 +3646,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -3630,6 +3706,17 @@ dependencies = [ "cfg-if 1.0.1", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "interprocess" version = "2.2.3" @@ -3703,6 +3790,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -3863,6 +3959,16 @@ dependencies = [ "uuid 1.17.0", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.8" @@ -3976,6 +4082,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lrtable" version = "0.13.10" @@ -4093,6 +4208,16 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if 1.0.1", + "rayon", +] + [[package]] name = "memchr" version = "2.7.5" @@ -4383,7 +4508,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustfft", "safetensors 0.6.1", - "schemars", + "schemars 0.8.22", "scraper", "serde", "serde-big-array", @@ -4702,6 +4827,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "ntapi" version = "0.4.1" @@ -4776,6 +4907,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -4948,6 +5090,30 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openai-harmony" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fd6b7d01a317c58d85a22b7cc314e14a2fef0dfdb93b819738d09caece16" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bstr", + "clap 4.5.42", + "fancy-regex 0.13.0", + "futures", + "image", + "regex", + "reqwest 0.12.22", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "serde_with", + "sha1", + "sha2", + "thiserror 2.0.12", +] + [[package]] name = "openssl" version = "0.10.73" @@ -5437,6 +5603,25 @@ dependencies = [ "yansi", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "prometheus" version = "0.14.0" @@ -5777,6 +5962,56 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if 1.0.1", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-cpuid" version = "10.7.0" @@ -5873,6 +6108,26 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "regex" version = "1.11.1" @@ -6025,6 +6280,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "ring" version = "0.17.14" @@ -6418,6 +6679,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -6663,6 +6948,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -6824,6 +7141,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "similar" version = "2.7.0" @@ -8313,6 +8639,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "validator" version = "0.20.0" diff --git a/deny.toml b/deny.toml index 73942131ea..dd2215755d 100644 --- a/deny.toml +++ b/deny.toml @@ -32,7 +32,8 @@ allow = [ "BSL-1.0", "MPL-2.0", "CDLA-Permissive-2.0", - "Zlib" + "Zlib", + "NCSA" ] # TODO exceptions diff --git a/lib/bindings/python/Cargo.lock b/lib/bindings/python/Cargo.lock index 105e3e88a3..4485a42ae7 100644 --- a/lib/bindings/python/Cargo.lock +++ b/lib/bindings/python/Cargo.lock @@ -46,6 +46,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1763692fc1416554cf051efc56a3de5595eca47299d731cc5c2b583adf8b4d2f" +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -132,6 +141,17 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -290,6 +310,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.13.3" @@ -500,21 +543,42 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -527,6 +591,12 @@ version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "blake3" version = "1.8.2" @@ -560,6 +630,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata 0.4.9", + "serde", +] + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.19.0" @@ -592,6 +679,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -711,6 +804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -725,6 +819,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "clap_lex" version = "0.7.5" @@ -740,6 +846,12 @@ dependencies = [ "cc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1188,6 +1300,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "dyn-stack" version = "0.10.0" @@ -1310,9 +1428,12 @@ version = "0.4.1" dependencies = [ "anyhow", "dynamo-async-openai", + "lazy_static", + "openai-harmony", "regex", "serde", "serde_json", + "tokio", "tracing", "uuid", ] @@ -1488,6 +1609,26 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1571,13 +1712,39 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set 0.5.3", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + [[package]] name = "fancy-regex" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -1588,6 +1755,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -2070,13 +2246,23 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ac5654356c6f7f6116905aeaf92ab002c3d03414ada5dbe0bb2e32aa5fea173" dependencies = [ - "fancy-regex", + "fancy-regex 0.14.0", "ggml-quants", - "indexmap", + "indexmap 2.10.0", "log", "num_enum", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -2101,7 +2287,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2122,6 +2308,12 @@ dependencies = [ "rand_distr", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -2146,6 +2338,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hf-hub" version = "0.4.3" @@ -2447,6 +2645,56 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.10.0" @@ -2455,6 +2703,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.5", + "serde", ] [[package]] @@ -2491,6 +2740,17 @@ dependencies = [ "cfg-if 1.0.1", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "inventory" version = "0.3.20" @@ -2585,6 +2845,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + [[package]] name = "js-sys" version = "0.3.77" @@ -2627,12 +2893,28 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.8" @@ -2705,6 +2987,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -2752,6 +3043,16 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if 1.0.1", + "rayon", +] + [[package]] name = "memchr" version = "2.7.5" @@ -2842,6 +3143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2964,6 +3266,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nid" version = "3.0.0" @@ -3047,6 +3355,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3106,6 +3420,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -3253,6 +3578,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "openai-harmony" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fd6b7d01a317c58d85a22b7cc314e14a2fef0dfdb93b819738d09caece16" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bstr", + "clap", + "fancy-regex 0.13.0", + "futures", + "image", + "regex", + "reqwest", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "serde_with", + "sha1", + "sha2", + "thiserror 2.0.15", +] + [[package]] name = "openssl-probe" version = "0.1.6" @@ -3363,7 +3712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.10.0", ] [[package]] @@ -3421,12 +3770,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap", + "indexmap 2.10.0", "quick-xml", "serde", "time", ] +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -3529,6 +3891,25 @@ dependencies = [ "yansi", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.106", +] + [[package]] name = "prometheus" version = "0.14.0" @@ -3743,6 +4124,21 @@ dependencies = [ "serde", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.38.1" @@ -3891,6 +4287,56 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if 1.0.1", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-cpuid" version = "10.7.0" @@ -3972,6 +4418,26 @@ dependencies = [ "thiserror 2.0.15", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "regex" version = "1.11.1" @@ -4031,6 +4497,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -4084,6 +4551,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "ring" version = "0.17.14" @@ -4325,6 +4798,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -4421,6 +4918,7 @@ version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -4478,6 +4976,49 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.1", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -4541,6 +5082,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "slab" version = "0.4.11" @@ -4823,6 +5379,17 @@ dependencies = [ "cfg-if 1.0.1", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.41" @@ -4906,7 +5473,7 @@ dependencies = [ "dary_heap", "derive_builder", "esaxx-rs", - "fancy-regex", + "fancy-regex 0.14.0", "getrandom 0.3.3", "hf-hub", "itertools 0.14.0", @@ -5071,7 +5638,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", @@ -5137,7 +5704,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.10.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5425,6 +5992,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "validator" version = "0.20.0" @@ -5629,6 +6207,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + [[package]] name = "which" version = "4.4.2" @@ -6131,7 +6715,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap", + "indexmap 2.10.0", "num_enum", "thiserror 1.0.69", ] @@ -6157,3 +6741,27 @@ dependencies = [ "system-deps", "zeromq-src", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" +dependencies = [ + "zune-core", +] diff --git a/lib/llm/src/engines.rs b/lib/llm/src/engines.rs index 59c5e15c21..c60956c892 100644 --- a/lib/llm/src/engines.rs +++ b/lib/llm/src/engines.rs @@ -204,12 +204,12 @@ impl for c in prompt.chars() { // we are returning characters not tokens, so there will be some postprocessing overhead tokio::time::sleep(*TOKEN_ECHO_DELAY).await; - let response = deltas.create_choice(0, Some(c.to_string()), None, None); + let response = deltas.create_choice(0, Some(c.to_string()), None, None, None); yield Annotated{ id: Some(id.to_string()), data: Some(response), event: None, comment: None }; id += 1; } - let response = deltas.create_choice(0, None, Some(dynamo_async_openai::types::FinishReason::Stop), None); + let response = deltas.create_choice(0, None, None, Some(dynamo_async_openai::types::FinishReason::Stop), None); yield Annotated { id: Some(id.to_string()), data: Some(response), event: None, comment: None }; }; diff --git a/lib/llm/src/protocols/openai/chat_completions/delta.rs b/lib/llm/src/protocols/openai/chat_completions/delta.rs index dad744959e..e023f54300 100644 --- a/lib/llm/src/protocols/openai/chat_completions/delta.rs +++ b/lib/llm/src/protocols/openai/chat_completions/delta.rs @@ -91,6 +91,7 @@ impl DeltaGenerator { // Reasoning parser type // This is hardcoded for now, but can be made configurable later. // TODO: Make parser type configurable once front-end integration is determined + // Change to GptOss to test GptOSS parser let reasoning_parser_type = ReasoningParserType::Basic; // Reasoning parser wrapper @@ -121,7 +122,7 @@ impl DeltaGenerator { pub fn create_logprobs( &self, tokens: Vec, - token_ids: Vec, + token_ids: &[TokenIdType], logprobs: Option, top_logprobs: Option, ) -> Option { @@ -132,7 +133,7 @@ impl DeltaGenerator { let toks = tokens .into_iter() .zip(token_ids) - .map(|(token, token_id)| (token.unwrap_or_default(), token_id)) + .map(|(token, token_id)| (token.unwrap_or_default(), *token_id)) .collect::>(); let tok_lps = toks .iter() @@ -183,11 +184,18 @@ impl DeltaGenerator { }) } - fn create_reasoning_content(&mut self, text: Option) -> Option { - let text = text?; + fn create_reasoning_content( + &mut self, + text: &Option, + token_ids: &[u32], + ) -> Option { + let text_ref = text.as_deref().unwrap_or(""); + if text_ref.is_empty() && token_ids.is_empty() { + return None; + } let parser_result = self .reasoning_parser - .parse_reasoning_streaming_incremental(&text); + .parse_reasoning_streaming_incremental(text_ref, token_ids); Some(parser_result) } @@ -207,17 +215,12 @@ impl DeltaGenerator { &mut self, index: u32, text: Option, + reasoning_content: Option, finish_reason: Option, logprobs: Option, ) -> NvCreateChatCompletionStreamResponse { - let reasoning_parser_result = self.create_reasoning_content(text).unwrap_or_default(); - - let (normal_text, reasoning_content) = ( - reasoning_parser_result.get_some_normal_text(), - reasoning_parser_result.get_some_reasoning(), - ); let delta = dynamo_async_openai::types::ChatCompletionStreamResponseDelta { - content: normal_text, + content: text, function_call: None, tool_calls: None, role: if self.msg_counter == 0 { @@ -292,7 +295,7 @@ impl crate::protocols::openai::DeltaGeneratorExt None, }; + let reasoning_parser_result = self + .create_reasoning_content(&delta.text, &delta.token_ids) + .unwrap_or_default(); + + let (normal_text, reasoning_content) = ( + reasoning_parser_result.get_some_normal_text(), + reasoning_parser_result.get_some_reasoning(), + ); + // Create the streaming response. let index = 0; - let stream_response = self.create_choice(index, delta.text, finish_reason, logprobs); + let stream_response = self.create_choice( + index, + normal_text, + reasoning_content, + finish_reason, + logprobs, + ); Ok(stream_response) } diff --git a/lib/llm/tests/http-service.rs b/lib/llm/tests/http-service.rs index 5c7ffc0b51..4e70ae0796 100644 --- a/lib/llm/tests/http-service.rs +++ b/lib/llm/tests/http-service.rs @@ -100,7 +100,7 @@ impl let stream = stream! { tokio::time::sleep(std::time::Duration::from_millis(max_tokens)).await; for i in 0..10 { - let output = generator.create_choice(i,Some(format!("choice {i}")), None, None); + let output = generator.create_choice(i,Some(format!("choice {i}")), None, None, None); yield Annotated::from_data(output); } diff --git a/lib/parsers/Cargo.toml b/lib/parsers/Cargo.toml index d9898c5fe8..3ceb79bb43 100644 --- a/lib/parsers/Cargo.toml +++ b/lib/parsers/Cargo.toml @@ -32,4 +32,6 @@ serde_json = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } -regex = "1" \ No newline at end of file +regex = "1" +openai-harmony = "0.0.3" +lazy_static = "1.5.0" diff --git a/lib/parsers/src/reasoning/base_parser.rs b/lib/parsers/src/reasoning/base_parser.rs index 96046b3962..da0afecdc0 100644 --- a/lib/parsers/src/reasoning/base_parser.rs +++ b/lib/parsers/src/reasoning/base_parser.rs @@ -33,7 +33,7 @@ impl BasicReasoningParser { } impl ReasoningParser for BasicReasoningParser { - fn detect_and_parse_reasoning(&self, text: &str) -> ParserResult { + fn detect_and_parse_reasoning(&mut self, text: &str, _token_ids: &[u32]) -> ParserResult { log::debug!("detect_and_parse_reasoning called with text: {:?}", text); let in_reasoning = self._in_reasoning || text.contains(&self.think_start_token); @@ -82,7 +82,11 @@ impl ReasoningParser for BasicReasoningParser { } } - fn parse_reasoning_streaming_incremental(&mut self, text: &str) -> ParserResult { + fn parse_reasoning_streaming_incremental( + &mut self, + text: &str, + _token_ids: &[u32], + ) -> ParserResult { // Incrementally parse the streaming text self._buffer.push_str(text); let mut current_text = self._buffer.to_string(); @@ -180,26 +184,26 @@ mod tests { #[test] fn test_detect_and_parse_reasoning_reasoning() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); let result = - parser.detect_and_parse_reasoning("with reasoning and more text."); + parser.detect_and_parse_reasoning("with reasoning and more text.", &[]); assert_eq!(result.normal_text, "and more text."); assert_eq!(result.reasoning_text, "with reasoning"); } #[test] fn test_detect_and_parse_reasoning_reasoning_no_reasoning() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); - let result = parser.detect_and_parse_reasoning("This is a test without reasoning."); + let result = parser.detect_and_parse_reasoning("This is a test without reasoning.", &[]); assert_eq!(result.normal_text, "This is a test without reasoning."); assert_eq!(result.reasoning_text, ""); } #[test] fn test_detect_and_parse_reasoning_reasoning_truncated_reasoning() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); - let result = parser.detect_and_parse_reasoning("with truncated reasoning"); + let result = parser.detect_and_parse_reasoning("with truncated reasoning", &[]); assert_eq!(result.normal_text, ""); assert_eq!(result.reasoning_text, "with truncated reasoning"); } @@ -208,7 +212,7 @@ mod tests { fn test_parse_reasoning_streaming_incremental() { let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); - let result = parser.parse_reasoning_streaming_incremental("".to_string(), "".to_string(), false, true); - let result = parser - .parse_reasoning_streaming_incremental("with reasoning and more text."); + let result = parser.parse_reasoning_streaming_incremental( + "with reasoning and more text.", + &[], + ); assert_eq!(result.normal_text, " and more text."); assert_eq!(result.reasoning_text, "with reasoning"); } @@ -227,17 +233,18 @@ mod tests { fn test_parse_reasoning_streaming_incremental_no_end_token() { let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), true, true); - let result = parser.parse_reasoning_streaming_incremental("with reasoning"); + let result = parser.parse_reasoning_streaming_incremental("with reasoning", &[]); assert_eq!(result.normal_text, ""); assert_eq!(result.reasoning_text, "with reasoning"); } #[test] fn test_detect_and_parse_reasoning_multiple_reasoning_blocks() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); let result = parser.detect_and_parse_reasoning( "first reasoning middle second reasoning end", + &[], ); // The current implementation only handles the first occurrence properly assert_eq!(result.normal_text, "middle second reasoning end"); @@ -248,14 +255,14 @@ mod tests { fn test_streaming_multiple_reasoning_blocks() { let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, false); - let result1 = - parser.parse_reasoning_streaming_incremental("first reasoning middle"); + let result1 = parser + .parse_reasoning_streaming_incremental("first reasoning middle", &[]); assert_eq!(result1.normal_text, " middle"); assert_eq!(result1.reasoning_text, "first reasoning"); // Basic parser assumes only one reasoning block at a time - let result2 = - parser.parse_reasoning_streaming_incremental(" second reasoning end"); + let result2 = parser + .parse_reasoning_streaming_incremental(" second reasoning end", &[]); assert_eq!(result2.normal_text, " second reasoning end"); assert_eq!(result2.reasoning_text, ""); } @@ -266,13 +273,15 @@ mod tests { BasicReasoningParser::new("".to_string(), "".to_string(), false, true); // Feed partial opening tag - let result1 = parser.parse_reasoning_streaming_incremental("reasoning content normal text"); + let result2 = parser.parse_reasoning_streaming_incremental( + "ink>reasoning content normal text", + &[], + ); assert_eq!(result2.normal_text, " normal text"); assert_eq!(result2.reasoning_text, "reasoning content"); } @@ -283,12 +292,13 @@ mod tests { BasicReasoningParser::new("".to_string(), "".to_string(), false, false); // Start with complete opening and partial content - let result1 = parser.parse_reasoning_streaming_incremental("reasoning contentreasoning content normal text"); + let result2 = parser.parse_reasoning_streaming_incremental("ink> normal text", &[]); assert_eq!(result2.normal_text, " normal text"); assert_eq!(result2.reasoning_text, "reasoning content"); } @@ -299,22 +309,22 @@ mod tests { BasicReasoningParser::new("".to_string(), "".to_string(), false, false); // First call - partial opening tag - let result1 = parser.parse_reasoning_streaming_incremental("part1 "); + let result2 = parser.parse_reasoning_streaming_incremental("ink>part1 ", &[]); assert_eq!(result2.normal_text, ""); assert_eq!(result2.reasoning_text, ""); // Third call - more reasoning content - let result3 = parser.parse_reasoning_streaming_incremental("part2 "); + let result3 = parser.parse_reasoning_streaming_incremental("part2 ", &[]); assert_eq!(result3.normal_text, ""); assert_eq!(result3.reasoning_text, ""); // Fourth call - end reasoning and normal text - let result4 = parser.parse_reasoning_streaming_incremental("part3 normal"); + let result4 = parser.parse_reasoning_streaming_incremental("part3 normal", &[]); assert_eq!(result4.normal_text, " normal"); assert_eq!(result4.reasoning_text, "part1 part2 part3"); } @@ -325,27 +335,28 @@ mod tests { BasicReasoningParser::new("".to_string(), "".to_string(), false, true); // Start reasoning block - let result1 = parser.parse_reasoning_streaming_incremental("reasoning "); + let result1 = parser.parse_reasoning_streaming_incremental("reasoning ", &[]); assert_eq!(result1.normal_text, ""); assert_eq!(result1.reasoning_text, "reasoning "); // Continue streaming reasoning - let result2 = parser.parse_reasoning_streaming_incremental("content "); + let result2 = parser.parse_reasoning_streaming_incremental("content ", &[]); assert_eq!(result2.normal_text, ""); assert_eq!(result2.reasoning_text, "content "); // End reasoning block - let result3 = parser.parse_reasoning_streaming_incremental("more normal"); + let result3 = parser.parse_reasoning_streaming_incremental("more normal", &[]); assert_eq!(result3.normal_text, " normal"); assert_eq!(result3.reasoning_text, "more"); } #[test] fn test_nested_reasoning_blocks() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); let result = parser.detect_and_parse_reasoning( "outer inner reasoning normal", + &[], ); // Current implementation should handle this by finding the first closing tag assert_eq!(result.normal_text, "reasoning normal"); @@ -355,28 +366,28 @@ mod tests { #[test] fn test_malformed_missing_closing_tag() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); - let result = parser.detect_and_parse_reasoning("reasoning without closing tag"); + let result = parser.detect_and_parse_reasoning("reasoning without closing tag", &[]); assert_eq!(result.normal_text, ""); assert_eq!(result.reasoning_text, "reasoning without closing tag"); } #[test] fn test_malformed_stray_closing_tag() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); - let result = parser.detect_and_parse_reasoning("normal text more normal"); + let result = parser.detect_and_parse_reasoning("normal text more normal", &[]); assert_eq!(result.normal_text, "normal text more normal"); assert_eq!(result.reasoning_text, ""); } #[test] fn test_malformed_multiple_opening_tags() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); let result = parser - .detect_and_parse_reasoning("first second reasoning normal"); + .detect_and_parse_reasoning("first second reasoning normal", &[]); // Should handle by replacing all opening tags and using first closing tag assert_eq!(result.normal_text, "normal"); assert_eq!(result.reasoning_text, "first second reasoning"); @@ -384,27 +395,27 @@ mod tests { #[test] fn test_empty_reasoning_block() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); - let result = parser.detect_and_parse_reasoning(" normal text"); + let result = parser.detect_and_parse_reasoning(" normal text", &[]); assert_eq!(result.normal_text, "normal text"); assert_eq!(result.reasoning_text, ""); } #[test] fn test_whitespace_only_reasoning_block() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), false, true); - let result = parser.detect_and_parse_reasoning(" \n\t normal text"); + let result = parser.detect_and_parse_reasoning(" \n\t normal text", &[]); assert_eq!(result.normal_text, "normal text"); assert_eq!(result.reasoning_text, ""); // Should be empty after trim } #[test] fn test_force_reasoning_mode() { - let parser = + let mut parser = BasicReasoningParser::new("".to_string(), "".to_string(), true, true); - let result = parser.detect_and_parse_reasoning("no think tags here"); + let result = parser.detect_and_parse_reasoning("no think tags here", &[]); assert_eq!(result.normal_text, ""); assert_eq!(result.reasoning_text, "no think tags here"); } @@ -416,19 +427,19 @@ mod tests { // Process complete reasoning block let result1 = - parser.parse_reasoning_streaming_incremental("reasoning normal"); + parser.parse_reasoning_streaming_incremental("reasoning normal", &[]); assert_eq!(result1.normal_text, " normal"); assert_eq!(result1.reasoning_text, "reasoning"); // Process normal text - should not be affected by previous state - let result2 = parser.parse_reasoning_streaming_incremental(" more normal text"); + let result2 = parser.parse_reasoning_streaming_incremental(" more normal text", &[]); assert_eq!(result2.normal_text, " more normal text"); assert_eq!(result2.reasoning_text, ""); // Basic parser does not expect more than one reasoning block at a time // So this should not affect the state - let result3 = - parser.parse_reasoning_streaming_incremental(" new reasoning final"); + let result3 = parser + .parse_reasoning_streaming_incremental(" new reasoning final", &[]); assert_eq!(result3.normal_text, " new reasoning final"); assert_eq!(result3.reasoning_text, ""); } diff --git a/lib/parsers/src/reasoning/deepseek_r1_parser.rs b/lib/parsers/src/reasoning/deepseek_r1_parser.rs index 22d9a33a93..529cdf4233 100644 --- a/lib/parsers/src/reasoning/deepseek_r1_parser.rs +++ b/lib/parsers/src/reasoning/deepseek_r1_parser.rs @@ -24,11 +24,16 @@ impl DeepseekR1ReasoningParser { } impl ReasoningParser for DeepseekR1ReasoningParser { - fn parse_reasoning_streaming_incremental(&mut self, text: &str) -> ParserResult { - self.base.parse_reasoning_streaming_incremental(text) + fn parse_reasoning_streaming_incremental( + &mut self, + text: &str, + token_ids: &[u32], + ) -> ParserResult { + self.base + .parse_reasoning_streaming_incremental(text, token_ids) } - fn detect_and_parse_reasoning(&self, text: &str) -> ParserResult { - self.base.detect_and_parse_reasoning(text) + fn detect_and_parse_reasoning(&mut self, text: &str, token_ids: &[u32]) -> ParserResult { + self.base.detect_and_parse_reasoning(text, token_ids) } } diff --git a/lib/parsers/src/reasoning/gpt_oss_parser.rs b/lib/parsers/src/reasoning/gpt_oss_parser.rs new file mode 100644 index 0000000000..77b629f1f5 --- /dev/null +++ b/lib/parsers/src/reasoning/gpt_oss_parser.rs @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Debug; + +use crate::ParserResult; +use crate::ReasoningParser; + +use openai_harmony::StreamableParser; +use openai_harmony::chat::TextContent; +use openai_harmony::{HarmonyEncoding, HarmonyEncodingName, chat::Role, load_harmony_encoding}; + +///// Static initialization of harmony encoder to not affect performance every time a parser is created +/// This is because load_harmony_encoding downloads some tiktoken files into a directory and we don't want to do this every time we create a parser. +use std::sync::OnceLock; + +static GLOBAL_HARMONY_GPTOSS_ENCODING: OnceLock> = + OnceLock::new(); + +fn get_harmony_encoding() -> &'static Result { + GLOBAL_HARMONY_GPTOSS_ENCODING + .get_or_init(|| load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)) +} + +pub struct GptOssReasoningParser { + parser: StreamableParser, +} + +/// Implement Debug for GptOssReasoningParser separately because StreamableParser does not implement Debug +impl Debug for GptOssReasoningParser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GptOssReasoningParser") + .field("parser", &self.parser.state_json()) + .finish() + } +} + +impl GptOssReasoningParser { + pub fn new() -> anyhow::Result { + let parser = match get_harmony_encoding().as_ref() { + Ok(enc) => match StreamableParser::new(enc.clone(), Some(Role::Assistant)) { + Ok(p) => p, + Err(e) => { + tracing::warn!("Harmony StreamableParser init failed for GPT OSS: {e}"); + return Err(anyhow::anyhow!( + "Failed to load Harmony StreamableParser: {e}" + )); + } + }, + Err(e) => { + tracing::warn!("Failed to load Harmony encoding for GPT OSS: {e}"); + return Err(anyhow::anyhow!("Failed to load Harmony encoding: {e}")); + } + }; + Ok(Self { parser }) + } +} + +impl ReasoningParser for GptOssReasoningParser { + fn detect_and_parse_reasoning(&mut self, _text: &str, token_ids: &[u32]) -> ParserResult { + tracing::debug!( + "detect_and_parse_reasoning called with {} token_ids", + token_ids.len() + ); + + let parser = &mut self.parser; + + for (i, token_id) in token_ids.iter().enumerate() { + tracing::debug!( + "Processing token {} of {}: {}", + i + 1, + token_ids.len(), + token_id + ); + if let Err(e) = parser.process(*token_id) { + tracing::warn!("Harmony parse error for token_id {token_id}: {e}"); + return ParserResult::default(); + } + } + + let output_msgs = parser.messages(); + tracing::debug!("Parser has {} output messages", output_msgs.len()); + + match output_msgs.len() { + 0 => { + tracing::debug!("No output messages, using current content"); + let current = parser.current_content().unwrap_or_default(); + tracing::debug!("Current content length: {}", current.len()); + ParserResult { + normal_text: String::new(), + reasoning_text: current, + } + } + 1 => { + tracing::debug!("Single output message detected"); + let mut reasoning_text = String::new(); + if let Some(openai_harmony::chat::Content::Text(TextContent { text })) = + output_msgs[0].content.first() + { + reasoning_text.push_str(text); + tracing::debug!("Extracted reasoning text length: {}", reasoning_text.len()); + } + let current = parser.current_content().unwrap_or_default(); + tracing::debug!("Current content length: {}", current.len()); + ParserResult { + normal_text: current, + reasoning_text, + } + } + _ => { + tracing::debug!("Multiple output messages detected: {}", output_msgs.len()); + let mut reasoning_text = String::new(); + let mut normal_text = String::new(); + + // Loop until second last message + for (i, parse_msg) in output_msgs.iter().take(output_msgs.len() - 1).enumerate() { + tracing::debug!("Processing reasoning message {}", i + 1); + if let Some(openai_harmony::chat::Content::Text(TextContent { text })) = + parse_msg.content.first() + { + reasoning_text.push_str(text); + tracing::debug!("Added {} chars to reasoning text", text.len()); + } + } + + let last_msg = &output_msgs[output_msgs.len() - 1]; + tracing::debug!("Processing final message"); + + // Handle the last message + if let Some(openai_harmony::chat::Content::Text(TextContent { text })) = + last_msg.content.first() + { + normal_text.push_str(text); + tracing::debug!("Added {} chars to normal text", text.len()); + } + + tracing::debug!( + "Final result - normal_text: {} chars, reasoning_text: {} chars", + normal_text.len(), + reasoning_text.len() + ); + + ParserResult { + normal_text, + reasoning_text, + } + } + } + } + + fn parse_reasoning_streaming_incremental( + &mut self, + _text: &str, + token_ids: &[u32], + ) -> ParserResult { + tracing::debug!( + "parse_reasoning_streaming_incremental called with {} token_ids", + token_ids.len() + ); + + let parser: &mut StreamableParser = &mut self.parser; + for (i, token_id) in token_ids.iter().enumerate() { + tracing::debug!( + "Processing streaming token {} of {}: {}", + i + 1, + token_ids.len(), + token_id + ); + if let Err(e) = parser.process(*token_id) { + tracing::warn!("Harmony parse error for token_id {token_id}: {e}"); + return ParserResult::default(); + } + } + + if let Some(channel) = self.parser.current_channel() { + tracing::debug!("Current channel: {}", channel); + if channel == "final" { + tracing::debug!("In final channel, processing normal text"); + // If we're in the final channel, we should not parse reasoning + if let Some(current) = self.parser.last_content_delta().unwrap_or_default() { + tracing::debug!("Got normal text delta of {} chars", current.len()); + return ParserResult { + normal_text: current, + reasoning_text: String::new(), + }; + } + tracing::debug!("No content delta in final channel"); + ParserResult::default() + } else { + tracing::debug!("In reasoning channel: {}", channel); + if let Some(current) = self.parser.last_content_delta().unwrap_or_default() { + tracing::debug!("Got reasoning text delta of {} chars", current.len()); + return ParserResult { + normal_text: String::new(), + reasoning_text: current, + }; + } + tracing::debug!("No content delta in reasoning channel"); + ParserResult::default() + } + } else { + tracing::debug!("No current channel detected"); + ParserResult::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gpt_oss_reasoning_parser() { + let mut parser = GptOssReasoningParser::new().expect("Failed to create parser"); + let enc = get_harmony_encoding() + .as_ref() + .expect("Failed to get encoding"); + let text = "<|channel|>analysis<|message|>The user asks a simple factual question: capital of Brazil. The answer is Brasília. No additional explanation needed.<|end|><|start|>assistant<|channel|>final<|message|>The capital of Brazil is Brasília."; + let token_ids = enc.tokenizer().encode_with_special_tokens(text); // Example token IDs + let result = parser.detect_and_parse_reasoning("Test text", &token_ids); + assert!(result.normal_text == "The capital of Brazil is Brasília."); + assert!( + result.reasoning_text + == "The user asks a simple factual question: capital of Brazil. The answer is Brasília. No additional explanation needed." + ); + } + + #[test] + fn test_gpt_oss_reasoning_parser_streaming() { + let mut parser = GptOssReasoningParser::new().expect("Failed to create parser"); + let enc = get_harmony_encoding() + .as_ref() + .expect("Failed to get encoding"); + let text = "<|channel|>analysis<|message|>The user asks a simple factual question: capital of Brazil. The answer is Brasília. No additional explanation needed.<|end|><|start|>assistant<|channel|>final<|message|>The capital of Brazil is Brasília."; + let token_ids = enc.tokenizer().encode_with_special_tokens(text); // Example token IDs + let mut reasoning_text_incr = String::new(); + let mut normal_text_incr = String::new(); + for token in token_ids.iter() { + let result = parser.parse_reasoning_streaming_incremental("Test text", &[(*token)]); + normal_text_incr.push_str(&result.normal_text); + reasoning_text_incr.push_str(&result.reasoning_text); + } + assert!(normal_text_incr == "The capital of Brazil is Brasília."); + assert!( + reasoning_text_incr + == "The user asks a simple factual question: capital of Brazil. The answer is Brasília. No additional explanation needed." + ); + } +} diff --git a/lib/parsers/src/reasoning/mod.rs b/lib/parsers/src/reasoning/mod.rs index 10975c0919..070d3656b5 100644 --- a/lib/parsers/src/reasoning/mod.rs +++ b/lib/parsers/src/reasoning/mod.rs @@ -3,10 +3,12 @@ mod base_parser; mod deepseek_r1_parser; +mod gpt_oss_parser; // Re-export main types and functions for convenience pub use base_parser::BasicReasoningParser; pub use deepseek_r1_parser::DeepseekR1ReasoningParser; +pub use gpt_oss_parser::GptOssReasoningParser; #[derive(Debug, Clone, Default)] pub struct ParserResult { @@ -39,12 +41,16 @@ pub trait ReasoningParser: Send + std::fmt::Debug { /// Parses a standalone, non-streaming input chunk. Implementations may reset or ignore /// internal streaming state and should return the split of normal vs reasoning text for /// this complete input. Marker tokens must not be included in either output. - fn detect_and_parse_reasoning(&self, text: &str) -> ParserResult; + fn detect_and_parse_reasoning(&mut self, text: &str, token_ids: &[u32]) -> ParserResult; /// Parses a streaming chunk and updates internal state. The return value should be the /// delta: only the newly discovered normal and reasoning text attributable to this chunk /// (not the cumulative totals). Marker tokens must not be included in either output. - fn parse_reasoning_streaming_incremental(&mut self, text: &str) -> ParserResult; + fn parse_reasoning_streaming_incremental( + &mut self, + text: &str, + token_ids: &[u32], + ) -> ParserResult; } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -52,6 +58,7 @@ pub trait ReasoningParser: Send + std::fmt::Debug { pub enum ReasoningParserType { DeepseekR1, Basic, + GptOss, } #[derive(std::fmt::Debug)] @@ -60,12 +67,17 @@ pub struct ReasoningParserWrapper { } impl ReasoningParser for ReasoningParserWrapper { - fn detect_and_parse_reasoning(&self, text: &str) -> ParserResult { - self.parser.detect_and_parse_reasoning(text) + fn detect_and_parse_reasoning(&mut self, text: &str, token_ids: &[u32]) -> ParserResult { + self.parser.detect_and_parse_reasoning(text, token_ids) } - fn parse_reasoning_streaming_incremental(&mut self, text: &str) -> ParserResult { - self.parser.parse_reasoning_streaming_incremental(text) + fn parse_reasoning_streaming_incremental( + &mut self, + text: &str, + token_ids: &[u32], + ) -> ParserResult { + self.parser + .parse_reasoning_streaming_incremental(text, token_ids) } } @@ -83,6 +95,24 @@ impl ReasoningParserType { true, )), }, + ReasoningParserType::GptOss => match GptOssReasoningParser::new() { + Ok(parser) => ReasoningParserWrapper { + parser: Box::new(parser), + }, + Err(e) => { + tracing::warn!( + "GptOssReasoningParser could not be initialized, falling back to Basic Reasoning Parser: {e}" + ); + ReasoningParserWrapper { + parser: Box::new(BasicReasoningParser::new( + "".into(), + "".into(), + false, + true, + )), + } + } + }, } } } From 26aac034a914a09d0c6effd6c4b2dc51c00ad48e Mon Sep 17 00:00:00 2001 From: Dmitry Tokarev Date: Mon, 25 Aug 2025 13:13:51 -0400 Subject: [PATCH 23/82] chore: vllm 0.10.1.1 (#2641) Signed-off-by: Jason Zhou --- container/Dockerfile.vllm | 6 +++--- container/deps/vllm/install_vllm.sh | 16 ++++++++-------- pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/container/Dockerfile.vllm b/container/Dockerfile.vllm index 12786af768..4341c63e31 100644 --- a/container/Dockerfile.vllm +++ b/container/Dockerfile.vllm @@ -13,11 +13,11 @@ ARG RUNTIME_IMAGE="nvcr.io/nvidia/cuda" ARG RUNTIME_IMAGE_TAG="12.8.1-runtime-ubuntu24.04" # Make sure to update the dependency version in pyproject.toml when updating this -ARG VLLM_REF="aab549870df50edf0512f0a59b574f692f546465" # from v0.10.1 +ARG VLLM_REF="1da94e673c257373280026f75ceb4effac80e892" # from v0.10.1.1 ARG TORCH_BACKEND="cu128" -# Match 0.10.1 vLLM release -# https://github.com/vllm-project/vllm/releases/tag/v0.10.1 +# Match 0.10.1.1 vLLM release +# https://github.com/vllm-project/vllm/releases/tag/v0.10.1.1 # Pinned to commit before https://github.com/deepseek-ai/DeepGEMM/pull/112 for DeepGEMM which seems to break on H100: # "RuntimeError: Failed: CUDA runtime error csrc/jit/kernel_runtime.hpp:108 '98'" ARG DEEPGEMM_REF="f85ec64" diff --git a/container/deps/vllm/install_vllm.sh b/container/deps/vllm/install_vllm.sh index 376f83f8cd..d44b4ab57c 100755 --- a/container/deps/vllm/install_vllm.sh +++ b/container/deps/vllm/install_vllm.sh @@ -20,10 +20,10 @@ set -euo pipefail # Parse arguments EDITABLE=true -VLLM_REF="aab549870df50edf0512f0a59b574f692f546465" # from v0.10.1 +VLLM_REF="1da94e673c257373280026f75ceb4effac80e892" # from v0.10.1.1 # When updating above VLLM_REF make sure precompiled wheel file URL is correct. Run this command: # aws s3 ls s3://vllm-wheels/${VLLM_REF}/ --region us-west-2 --no-sign-request -VLLM_PRECOMPILED_WHEEL_LOCATION="https://vllm-wheels.s3.us-west-2.amazonaws.com/${VLLM_REF}/vllm-0.10.1-cp38-abi3-manylinux1_x86_64.whl" +VLLM_PRECOMPILED_WHEEL_LOCATION="https://vllm-wheels.s3.us-west-2.amazonaws.com/${VLLM_REF}/vllm-0.10.1.1-cp38-abi3-manylinux1_x86_64.whl" VLLM_GIT_URL="https://github.com/vllm-project/vllm.git" MAX_JOBS=16 INSTALLATION_DIR=/tmp @@ -86,13 +86,13 @@ while [[ $# -gt 0 ]]; do echo "Options:" echo " --editable Install vllm in editable mode (default)" echo " --no-editable Install vllm in non-editable mode" - echo f" --vllm-ref REF Git reference to checkout (default: ${VLLM_REF})" - echo f" --max-jobs NUM Maximum number of parallel jobs (default: ${MAX_JOBS})" + echo " --vllm-ref REF Git reference to checkout (default: ${VLLM_REF})" + echo " --max-jobs NUM Maximum number of parallel jobs (default: ${MAX_JOBS})" echo " --arch ARCH Architecture (amd64|arm64, default: auto-detect)" - echo f" --installation-dir DIR Directory to install vllm (default: ${INSTALLATION_DIR})" - echo f" --deepgemm-ref REF Git reference for DeepGEMM (default: ${DEEPGEMM_REF})" - echo f" --flashinf-ref REF Git reference for Flash Infer (default: ${FLASHINF_REF})" - echo f" --torch-backend BACKEND Torch backend to use (default: ${TORCH_BACKEND})" + echo " --installation-dir DIR Directory to install vllm (default: ${INSTALLATION_DIR})" + echo " --deepgemm-ref REF Git reference for DeepGEMM (default: ${DEEPGEMM_REF})" + echo " --flashinf-ref REF Git reference for Flash Infer (default: ${FLASHINF_REF})" + echo " --torch-backend BACKEND Torch backend to use (default: ${TORCH_BACKEND})" exit 0 ;; *) diff --git a/pyproject.toml b/pyproject.toml index 3e00ae4820..291120bd06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ trtllm =[ vllm = [ "uvloop", "nixl<=0.4.1", - "vllm[flashinfer]==0.10.1", + "vllm[flashinfer]==0.10.1.1", ] sglang = [ From 326d291f0461bca1d0354ab7d0c61bbe457a5c3e Mon Sep 17 00:00:00 2001 From: hhzhang16 <54051230+hhzhang16@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:35:36 -0400 Subject: [PATCH 24/82] feat: add prometheus to the runtime image for sglang (#2689) Signed-off-by: Jason Zhou --- container/Dockerfile.sglang | 3 +++ 1 file changed, 3 insertions(+) diff --git a/container/Dockerfile.sglang b/container/Dockerfile.sglang index 4c1bc8fcaa..b40f156c42 100644 --- a/container/Dockerfile.sglang +++ b/container/Dockerfile.sglang @@ -422,6 +422,9 @@ COPY --from=base /usr/local/bin/etcd/ /usr/local/bin/etcd/ # Add ETCD and CUDA binaries to PATH so cicc and other CUDA tools are accessible ENV PATH=/usr/local/bin/etcd/:/usr/local/cuda/nvvm/bin:$PATH +# Copy prometheus from base image +COPY --from=base /usr/local/bin/prometheus /usr/local/bin/prometheus + # Copy UCX from base image as plugin for NIXL # Copy NIXL source from wheel_builder image ARG ARCH_ALT From 77775bb6549f3073ec702d9fb63df994ae1240af Mon Sep 17 00:00:00 2001 From: Hyeonki Hong Date: Tue, 26 Aug 2025 03:28:47 +0900 Subject: [PATCH 25/82] feat: support HF_HOME/_ENDPOINT env for Hugging Face models (#2642) Signed-off-by: Hyeonki Hong Signed-off-by: Jason Zhou --- lib/llm/src/hub.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/llm/src/hub.rs b/lib/llm/src/hub.rs index 5957a4beea..bdeb36b8d0 100644 --- a/lib/llm/src/hub.rs +++ b/lib/llm/src/hub.rs @@ -42,7 +42,7 @@ fn is_weight_file(filename: &str) -> bool { pub async fn from_hf(name: impl AsRef, ignore_weights: bool) -> anyhow::Result { let name = name.as_ref(); let token = env::var(HF_TOKEN_ENV_VAR).ok(); - let api = ApiBuilder::new() + let api = ApiBuilder::from_env() .with_progress(true) .with_token(token) .high() From 183088d0142ee6c58a42e41346adaa3efffc4de7 Mon Sep 17 00:00:00 2001 From: Paul Hendricks Date: Mon, 25 Aug 2025 15:56:07 -0400 Subject: [PATCH 26/82] refactor: Switch ModelManager locks from `std::sync::Mutex` to `parking_lot::Mutex` (#2696) Signed-off-by: Jason Zhou --- Cargo.lock | 1 + lib/llm/Cargo.toml | 1 + lib/llm/src/discovery/model_manager.rs | 39 ++++++++++++-------------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 465c91c5ce..c684491c5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,6 +1999,7 @@ dependencies = [ "nixl-sys", "offset-allocator", "oneshot", + "parking_lot", "prometheus", "proptest", "rand 0.9.2", diff --git a/lib/llm/Cargo.toml b/lib/llm/Cargo.toml index 94e8920800..0f9a8c08b4 100644 --- a/lib/llm/Cargo.toml +++ b/lib/llm/Cargo.toml @@ -64,6 +64,7 @@ hf-hub = { workspace = true } humantime = { workspace = true } # input/batch rand = { workspace = true } oneshot = { workspace = true } +parking_lot = "0.12.4" prometheus = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/lib/llm/src/discovery/model_manager.rs b/lib/llm/src/discovery/model_manager.rs index 41523d6bfe..f1b7c38ba8 100644 --- a/lib/llm/src/discovery/model_manager.rs +++ b/lib/llm/src/discovery/model_manager.rs @@ -1,12 +1,18 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, RwLock}, +}; + +use parking_lot::Mutex; + use dynamo_runtime::component::Component; use dynamo_runtime::prelude::DistributedRuntimeProvider; use dynamo_runtime::slug::Slug; use crate::discovery::ModelEntry; - use crate::kv_router::{KvRouterConfig, scheduler::DefaultWorkerSelector}; use crate::{ kv_router::KvRouter, @@ -15,12 +21,6 @@ use crate::{ completions::OpenAICompletionsStreamingEngine, embeddings::OpenAIEmbeddingsStreamingEngine, }, }; -use std::collections::HashSet; -use std::sync::RwLock; -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; #[derive(Debug, thiserror::Error)] pub enum ModelManagerError { @@ -61,7 +61,7 @@ impl ModelManager { } pub fn get_model_entries(&self) -> Vec { - self.entries.lock().unwrap().values().cloned().collect() + self.entries.lock().values().cloned().collect() } pub fn has_model_any(&self, model: &str) -> bool { @@ -170,12 +170,12 @@ impl ModelManager { /// Save a ModelEntry under an instance's etcd `models/` key so we can fetch it later when the key is /// deleted from etcd. pub fn save_model_entry(&self, key: &str, entry: ModelEntry) { - self.entries.lock().unwrap().insert(key.to_string(), entry); + self.entries.lock().insert(key.to_string(), entry); } /// Remove and return model entry for this instance's etcd key. We do this when the instance stops. pub fn remove_model_entry(&self, key: &str) -> Option { - self.entries.lock().unwrap().remove(key) + self.entries.lock().remove(key) } pub async fn kv_chooser_for( @@ -203,7 +203,7 @@ impl ModelManager { } fn get_kv_chooser(&self, model_name: &str) -> Option> { - self.kv_choosers.lock().unwrap().get(model_name).cloned() + self.kv_choosers.lock().get(model_name).cloned() } /// Create and return a KV chooser for this component and model @@ -242,21 +242,18 @@ impl ModelManager { let new_kv_chooser = Arc::new(chooser); self.kv_choosers .lock() - .unwrap() .insert(model_name.to_string(), new_kv_chooser.clone()); Ok(new_kv_chooser) } pub fn get_model_tool_call_parser(&self, model: &str) -> Option { - match self.entries.lock() { - Ok(entries) => entries - .values() - .find(|entry| entry.name == model) - .and_then(|entry| entry.runtime_config.as_ref()) - .and_then(|config| config.tool_call_parser.clone()) - .map(|parser| parser.to_string()), - Err(_) => None, - } + self.entries + .lock() + .values() + .find(|entry| entry.name == model) + .and_then(|entry| entry.runtime_config.as_ref()) + .and_then(|config| config.tool_call_parser.clone()) + .map(|parser| parser.to_string()) } } From bdcbc560532a6b214bc17ed864f0bd693af2474d Mon Sep 17 00:00:00 2001 From: Yan Ru Pei Date: Mon, 25 Aug 2025 15:27:10 -0700 Subject: [PATCH 27/82] feat: python bindings for the entire KvPushRouter + per-request router configs (#2658) Signed-off-by: Jason Zhou --- Cargo.lock | 14 -- Cargo.toml | 1 - components/README.md | 9 - components/router/Cargo.toml | 37 --- components/router/src/main.rs | 98 -------- docs/components/router/README.md | 69 +++++- docs/hidden_toctree.rst | 1 - examples/basics/multinode/README.md | 2 - lib/bindings/python/Cargo.lock | 1 - lib/bindings/python/rust/lib.rs | 2 + lib/bindings/python/rust/llm/entrypoint.rs | 6 + lib/bindings/python/rust/llm/kv.rs | 188 ++++++++++++++ lib/llm/src/kv_router.rs | 23 +- lib/llm/src/kv_router/scheduler.rs | 23 +- lib/llm/src/migration.rs | 25 +- lib/llm/src/mocker/engine.rs | 65 ++--- lib/llm/src/protocols/common/preprocessor.rs | 5 + tests/router/test_router_e2e_with_mockers.py | 244 ++++++++++++++++++- 18 files changed, 595 insertions(+), 218 deletions(-) delete mode 100644 components/router/Cargo.toml delete mode 100644 components/router/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index c684491c5c..8326fa6e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6323,20 +6323,6 @@ dependencies = [ "serde", ] -[[package]] -name = "router" -version = "0.4.1" -dependencies = [ - "clap 4.5.42", - "dynamo-llm", - "dynamo-runtime", - "rand 0.9.2", - "serde", - "serde_json", - "tokio", - "tracing", -] - [[package]] name = "rstest" version = "0.18.2" diff --git a/Cargo.toml b/Cargo.toml index bd6620cf20..ea6eeee8d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ [workspace] members = [ "components/metrics", - "components/router", "launch/*", "lib/llm", "lib/runtime", diff --git a/components/README.md b/components/README.md index 394fbaf13e..dc943f5554 100644 --- a/components/README.md +++ b/components/README.md @@ -49,15 +49,6 @@ The frontend component provides the HTTP API layer and request processing: - **Router** - Routes requests to appropriate workers based on load and KV cache state - **Auto-discovery** - Automatically discovers and registers available workers -### [Router](router/) - -A high-performance request router written in Rust that: - -- Routes incoming requests to optimal workers based on KV cache state -- Implements KV-aware routing to minimize cache misses -- Provides load balancing across multiple worker instances -- Supports both aggregated and disaggregated serving patterns - ### [Planner](planner/) The planner component monitors system state and dynamically adjusts worker allocation: diff --git a/components/router/Cargo.toml b/components/router/Cargo.toml deleted file mode 100644 index a76493101f..0000000000 --- a/components/router/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -[package] -name = "router" -version.workspace = true -edition.workspace = true -description.workspace = true -authors.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -keywords.workspace = true - -[dependencies] -dynamo-runtime = { workspace = true} -dynamo-llm = { workspace = true} - -rand = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -clap = { version = "4.5", features = ["derive"] } diff --git a/components/router/src/main.rs b/components/router/src/main.rs deleted file mode 100644 index b5b2f8f7a8..0000000000 --- a/components/router/src/main.rs +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed 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. - -// TODO(#400): -// Instead of passing in a block_size, we should get this data from the backend component's config. -// What changes need to be made: -// 1. Take as an argument the name of the backend component. -// 2. Update the backend component to produce a config in a standard location. -// 3. Update the KvRouter to read the config from the backend component. - -use std::collections::HashMap; -use std::sync::Arc; - -use clap::Parser; - -use dynamo_llm::kv_router::{ - KvRouter, WorkerSelector, - protocols::WorkerSelectionResult, - scheduler::{DefaultWorkerSelector, KvSchedulerError, SchedulingRequest}, -}; -use dynamo_llm::local_model::runtime_config::ModelRuntimeConfig; -use dynamo_runtime::{ - DistributedRuntime, Result, Runtime, Worker, logging, pipeline::network::Ingress, -}; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Namespace for the distributed component - #[arg(long)] - namespace: String, - - /// Component name for the service - #[arg(long, default_value = "kv_aware_router")] - component: String, - - /// Block size for the router - #[arg(long)] - block_size: u32, -} - -fn main() -> Result<()> { - logging::init(); - let worker = Worker::from_settings()?; - worker.execute(app) -} - -async fn app(runtime: Runtime) -> Result<()> { - let args = Args::parse(); - let runtime = DistributedRuntime::from_settings(runtime).await?; - - let component = runtime - .namespace(&args.namespace)? - .component(&args.component)?; - - let selector = Box::new(CustomWorkerSelector::default()); - - let router = KvRouter::new(component.clone(), args.block_size, Some(selector), None).await?; - let router = Ingress::for_engine(Arc::new(router))?; - - component - .service_builder() - .create() - .await? - .endpoint("generate") - .endpoint_builder() - .handler(router) - .start() - .await -} - -#[derive(Default)] -pub struct CustomWorkerSelector(DefaultWorkerSelector); - -impl WorkerSelector for CustomWorkerSelector { - fn select_worker( - &self, - workers: &HashMap>, - request: &SchedulingRequest, - block_size: u32, - ) -> Result { - // customize logic here - // F12 into [DefaultWorkerSelector] to see the original logic - self.0.select_worker(workers, request, block_size) - } -} diff --git a/docs/components/router/README.md b/docs/components/router/README.md index b891ad2b19..e9d3ef91f6 100644 --- a/docs/components/router/README.md +++ b/docs/components/router/README.md @@ -143,4 +143,71 @@ The `router_temperature` parameter controls routing randomness: 3. Adjust `kv-overlap-score-weight` to meet your performance goals: - To reduce TTFT: Increase the weight - To reduce ITL: Decrease the weight -4. If you observe severe load imbalance, increase the temperature setting \ No newline at end of file +4. If you observe severe load imbalance, increase the temperature setting + +## Using KvPushRouter Python API + +Instead of launching the KV Router via command line, you can create a `KvPushRouter` object directly in Python. This allows per-request routing configuration overrides. + +### Setup + +First, launch your backend engines: +```bash +python -m dynamo.vllm --model meta-llama/Llama-2-7b-hf --endpoint dyn://inference.vllm.generate +``` + +### Example Script + +```python +import asyncio +from dynamo._core import DistributedRuntime, KvPushRouter, KvRouterConfig + +async def main(): + # Get runtime and create endpoint + runtime = DistributedRuntime.detached() + namespace = runtime.namespace("inference") + component = namespace.component("vllm") + endpoint = component.endpoint("generate") + + # Create KV router + kv_router_config = KvRouterConfig() + router = KvPushRouter( + endpoint=endpoint, + block_size=16, + kv_router_config=kv_router_config + ) + + # Your input tokens + token_ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + # Generate with per-request routing override + stream = await router.generate( + token_ids=token_ids, + model="meta-llama/Llama-2-7b-hf", + stop_conditions={ + "max_tokens": 20, # Generate exactly 20 tokens + "ignore_eos": True, # Don't stop at EOS token + }, + sampling_options={ + "temperature": 0.7, + "top_p": 0.9, + }, + router_config_override={ + "overlap_score_weight": 2.0, # Prioritize cache hits for this request + "router_temperature": 0.5, # Add routing randomness + } + ) + + # Collect generated tokens + generated_tokens = [] + async for response in stream: + if isinstance(response, dict) and "token_ids" in response: + generated_tokens.extend(response["token_ids"]) + + print(f"Generated {len(generated_tokens)} tokens: {generated_tokens}") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +The `router_config_override` parameter allows you to adjust routing behavior per request without recreating the router. This is useful for implementing different routing strategies based on request characteristics. \ No newline at end of file diff --git a/docs/hidden_toctree.rst b/docs/hidden_toctree.rst index 19a2ee7705..9b7e2d1b07 100644 --- a/docs/hidden_toctree.rst +++ b/docs/hidden_toctree.rst @@ -39,7 +39,6 @@ components/backends/sglang/docs/multinode-examples.md components/backends/sglang/docs/sgl-http-server.md components/backends/sglang/slurm_jobs/README.md - components/router/README.md examples/README.md guides/dynamo_deploy/create_deployment.md guides/dynamo_deploy/sla_planner_deployment.md diff --git a/examples/basics/multinode/README.md b/examples/basics/multinode/README.md index b2258ac709..de5c2c17ca 100644 --- a/examples/basics/multinode/README.md +++ b/examples/basics/multinode/README.md @@ -382,8 +382,6 @@ python -m dynamo.frontend \ However, for maximum performance with shared prefixes and multi-turn conversations, KV routing provides significant advantages by minimizing redundant computation. -For detailed router configuration and tuning options, see the [KV Router Documentation](../../../docs/components/router/README.md). - ## Monitoring and Debugging ### Check Worker Registration diff --git a/lib/bindings/python/Cargo.lock b/lib/bindings/python/Cargo.lock index 4485a42ae7..e48c2a59dc 100644 --- a/lib/bindings/python/Cargo.lock +++ b/lib/bindings/python/Cargo.lock @@ -1433,7 +1433,6 @@ dependencies = [ "regex", "serde", "serde_json", - "tokio", "tracing", "uuid", ] diff --git a/lib/bindings/python/rust/lib.rs b/lib/bindings/python/rust/lib.rs index 5989f076b1..800bddabc2 100644 --- a/lib/bindings/python/rust/lib.rs +++ b/lib/bindings/python/rust/lib.rs @@ -111,6 +111,8 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; engine::add_to_module(m)?; diff --git a/lib/bindings/python/rust/llm/entrypoint.rs b/lib/bindings/python/rust/llm/entrypoint.rs index 78e58b0f4c..1230ac6581 100644 --- a/lib/bindings/python/rust/llm/entrypoint.rs +++ b/lib/bindings/python/rust/llm/entrypoint.rs @@ -33,6 +33,12 @@ pub struct KvRouterConfig { inner: RsKvRouterConfig, } +impl KvRouterConfig { + pub fn inner(&self) -> RsKvRouterConfig { + self.inner + } +} + #[pymethods] impl KvRouterConfig { #[new] diff --git a/lib/bindings/python/rust/llm/kv.rs b/lib/bindings/python/rust/llm/kv.rs index 652e3a74b5..8b503b2dd7 100644 --- a/lib/bindings/python/rust/llm/kv.rs +++ b/lib/bindings/python/rust/llm/kv.rs @@ -13,8 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use pythonize::{depythonize, pythonize}; use std::collections::HashMap; use std::sync::atomic::AtomicU32; +use tokio_stream::StreamExt; use super::*; use llm_rs::kv_router::indexer::compute_block_hash_for_seq; @@ -28,6 +30,7 @@ use tracing; use llm_rs::kv_router::protocols::*; use llm_rs::kv_router::publisher::{create_stored_blocks, KvEventSourceConfig}; +use llm_rs::protocols::common::{OutputOptions, SamplingOptions, StopConditions}; #[pyfunction] pub fn compute_block_hash_for_seq_py(tokens: Vec, kv_block_size: usize) -> PyResult> { @@ -833,3 +836,188 @@ impl SpecDecodeStats { }) } } + +#[pyclass] +pub(crate) struct KvPushRouter { + inner: Arc, +} + +#[pymethods] +impl KvPushRouter { + #[new] + fn new( + endpoint: &Endpoint, + block_size: usize, + kv_router_config: &super::entrypoint::KvRouterConfig, + ) -> PyResult { + let runtime = pyo3_async_runtimes::tokio::get_runtime(); + runtime.block_on(async move { + let client = endpoint.inner.client().await.map_err(to_pyerr)?; + + // Create PushRouter with KV router mode + let push_router = rs::pipeline::PushRouter::< + llm_rs::protocols::common::preprocessor::PreprocessedRequest, + rs::protocols::annotated::Annotated< + llm_rs::protocols::common::llm_backend::LLMEngineOutput, + >, + >::from_client( + client, + rs::pipeline::network::egress::push_router::RouterMode::KV, + ) + .await + .map_err(to_pyerr)?; + + // Get component from endpoint + let component = endpoint.inner.component(); + + // Create KvRouter + let kv_router = llm_rs::kv_router::KvRouter::new( + component.clone(), + block_size as u32, + None, // default selector + Some(kv_router_config.inner()), + ) + .await + .map_err(to_pyerr)?; + + // Create KvPushRouter + let kv_push_router = + llm_rs::kv_router::KvPushRouter::new(push_router, Arc::new(kv_router)); + + Ok(Self { + inner: Arc::new(kv_push_router), + }) + }) + } + + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (token_ids, model, stop_conditions=None, sampling_options=None, output_options=None, router_config_override=None))] + fn generate<'p>( + &self, + py: Python<'p>, + token_ids: Vec, + model: String, + stop_conditions: Option, + sampling_options: Option, + output_options: Option, + router_config_override: Option, + ) -> PyResult> { + // Depythonize the options with defaults + let (stop_conditions, sampling_options, output_options, router_config_override) = + Python::with_gil(|py| { + let stop_conditions: StopConditions = if let Some(obj) = stop_conditions { + depythonize(obj.bind(py)).map_err(to_pyerr)? + } else { + StopConditions::default() + }; + + let sampling_options: SamplingOptions = if let Some(obj) = sampling_options { + depythonize(obj.bind(py)).map_err(to_pyerr)? + } else { + SamplingOptions::default() + }; + + let output_options: OutputOptions = if let Some(obj) = output_options { + depythonize(obj.bind(py)).map_err(to_pyerr)? + } else { + OutputOptions::default() + }; + + let router_config_override: Option = + if let Some(obj) = router_config_override { + Some(depythonize(obj.bind(py)).map_err(to_pyerr)?) + } else { + None + }; + + Ok::<_, PyErr>(( + stop_conditions, + sampling_options, + output_options, + router_config_override, + )) + })?; + + // Build the PreprocessedRequest + let request = llm_rs::protocols::common::preprocessor::PreprocessedRequest::builder() + .model(model) + .token_ids(token_ids) + .stop_conditions(stop_conditions) + .sampling_options(sampling_options) + .output_options(output_options) + .router_config_override(router_config_override) + .build() + .map_err(to_pyerr)?; + + let inner = self.inner.clone(); + + // Create a Python async generator that wraps the Rust stream + pyo3_async_runtimes::tokio::future_into_py(py, async move { + use rs::pipeline::{AsyncEngine, SingleIn}; + use tokio_stream::StreamExt; + + let single_in = SingleIn::new(request); + let stream = inner.generate(single_in).await.map_err(to_pyerr)?; + let (tx, rx) = tokio::sync::mpsc::channel(100); + + // Spawn a task to process the stream + tokio::spawn(async move { + let mut stream = stream; + while let Some(response) = stream.next().await { + // Convert LLMEngineOutput to PyObject + let py_response = Python::with_gil(|py| { + pythonize(py, &response.data) + .map(|obj| obj.unbind()) + .map_err(|e| e.to_string()) + }); + + match py_response { + Ok(obj) => { + if tx.send(obj).await.is_err() { + break; // Receiver dropped + } + } + Err(e) => { + tracing::error!("Failed to pythonize response: {}", e); + break; + } + } + } + }); + + // Return a Python async generator wrapper + Ok(KvPushRouterStream { + rx: Arc::new(tokio::sync::Mutex::new(rx)), + }) + }) + } +} + +// Python async generator wrapper for the stream +#[pyclass] +pub(crate) struct KvPushRouterStream { + rx: Arc>>, +} + +#[pymethods] +impl KvPushRouterStream { + #[pyo3(name = "__aiter__")] + fn aiter(slf: Bound<'_, Self>) -> PyResult> { + Ok(slf.clone().into_any().unbind()) + } + + #[pyo3(name = "__anext__")] + fn anext<'p>(&self, py: Python<'p>) -> PyResult> { + let rx = self.rx.clone(); + + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let mut rx = rx.lock().await; + match rx.recv().await { + Some(obj) => Ok(obj), + None => Err(pyo3::exceptions::PyStopAsyncIteration::new_err( + "Stream exhausted", + )), + } + }) + } +} diff --git a/lib/llm/src/kv_router.rs b/lib/llm/src/kv_router.rs index f81ca7b530..c37ee2244a 100644 --- a/lib/llm/src/kv_router.rs +++ b/lib/llm/src/kv_router.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; +use derive_builder::Builder; use dynamo_runtime::{ component::{Component, InstanceSource}, pipeline::{ @@ -73,6 +74,16 @@ pub trait WorkerSelector { ) -> Result; } +/// Override configuration for router settings that can be specified per-request +#[derive(Debug, Clone, Default, Builder, Serialize, Deserialize)] +pub struct RouterConfigOverride { + #[builder(default)] + pub overlap_score_weight: Option, + + #[builder(default)] + pub router_temperature: Option, +} + /// KV Router configuration parameters #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct KvRouterConfig { @@ -261,6 +272,7 @@ impl KvRouter { &self, context_id: &str, tokens: &[u32], + router_config_override: Option<&RouterConfigOverride>, ) -> anyhow::Result<(i64, u32)> { let isl_tokens = tokens.len(); @@ -276,6 +288,7 @@ impl KvRouter { isl_tokens, seq_hashes.clone(), overlap_scores.clone(), + router_config_override, ) .await?; @@ -315,7 +328,9 @@ impl AsyncEngine, ManyOut>, Er request: SingleIn, ) -> Result>> { let (request, ctx) = request.into_parts(); - let (worker_id, _) = self.find_best_match(ctx.id(), &request.tokens).await?; + let (worker_id, _) = self + .find_best_match(ctx.id(), &request.tokens, None) + .await?; let response = RouterResponse { worker_id }; let response = Annotated::from_data(response); @@ -357,7 +372,11 @@ impl AsyncEngine, ManyOut, pub prefill_tokens: HashMap, + // Router config overrides for this specific request + pub router_config_override: Option, // Option to take it out to send the response without moving the struct resp_tx: Option>, } @@ -243,6 +246,7 @@ impl KvScheduler { isl_tokens: usize, token_seq: Vec, overlaps: OverlapScores, + router_config_override: Option<&RouterConfigOverride>, ) -> Result { let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); let request = SchedulingRequest { @@ -252,6 +256,7 @@ impl KvScheduler { overlaps, decode_blocks: HashMap::new(), prefill_tokens: HashMap::new(), + router_config_override: router_config_override.cloned(), resp_tx: Some(resp_tx), // Wrap in Some() }; @@ -402,14 +407,19 @@ impl WorkerSelector for DefaultWorkerSelector { .unwrap_or(&(potential_prefill_block.floor() as usize)) as f64; + // Use override if provided, otherwise use default config + let overlap_weight = request + .router_config_override + .as_ref() + .and_then(|cfg| cfg.overlap_score_weight) + .unwrap_or(self.kv_router_config.overlap_score_weight); + // Calculate logit (lower is better) - let logit = - self.kv_router_config.overlap_score_weight * potential_prefill_block + decode_block; + let logit = overlap_weight * potential_prefill_block + decode_block; max_logit = max_logit.max(logit); worker_logits.insert(*worker_id, logit); - let overlap_weight = self.kv_router_config.overlap_score_weight; tracing::info!( "Formula for {worker_id} with {overlap} cached blocks: {logit:.3} \ = {overlap_weight:.1} * prefill_blocks + decode_blocks \ @@ -418,7 +428,12 @@ impl WorkerSelector for DefaultWorkerSelector { } // Use softmax sampling to select worker - let temperature = self.kv_router_config.router_temperature; + // Use override if provided, otherwise use default config + let temperature = request + .router_config_override + .as_ref() + .and_then(|cfg| cfg.router_temperature) + .unwrap_or(self.kv_router_config.router_temperature); let best_worker_id = softmax_sample(&worker_logits, temperature); let best_logit = worker_logits[&best_worker_id]; diff --git a/lib/llm/src/migration.rs b/lib/llm/src/migration.rs index 7136e0d8d6..d414fd28c7 100644 --- a/lib/llm/src/migration.rs +++ b/lib/llm/src/migration.rs @@ -176,22 +176,19 @@ mod tests { // Helper to create a mock preprocessed request fn create_mock_request(max_tokens: u32) -> PreprocessedRequest { - PreprocessedRequest { - model: "mock".to_string(), - token_ids: vec![1, 2, 3], - batch_token_ids: None, - stop_conditions: StopConditions { + PreprocessedRequest::builder() + .model("mock".to_string()) + .token_ids(vec![1, 2, 3]) + .stop_conditions(StopConditions { max_tokens: Some(max_tokens), ..Default::default() - }, - sampling_options: SamplingOptions::default(), - output_options: OutputOptions::default(), - eos_token_ids: vec![], - mdc_sum: None, - annotations: vec![], - estimated_prefix_hit_num_blocks: None, - backend_instance_id: None, - } + }) + .sampling_options(SamplingOptions::default()) + .output_options(OutputOptions::default()) + .eos_token_ids(vec![]) + .annotations(vec![]) + .build() + .unwrap() } // Helper to create mock LLM engine output diff --git a/lib/llm/src/mocker/engine.rs b/lib/llm/src/mocker/engine.rs index d1e63f42c7..347202e7f3 100644 --- a/lib/llm/src/mocker/engine.rs +++ b/lib/llm/src/mocker/engine.rs @@ -44,7 +44,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tokio::sync::{Mutex, OnceCell, mpsc}; -use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::wrappers::UnboundedReceiverStream; use uuid::Uuid; pub const MOCKER_COMPONENT: &str = "mocker"; @@ -366,7 +366,7 @@ impl AsyncEngine, ManyOut, Error> self.direct(direct_request, dp_rank as usize); // Create a simple channel for the stream - let (stream_tx, stream_rx) = mpsc::channel::(64); + let (stream_tx, stream_rx) = mpsc::unbounded_channel::(); let active_requests = self.active_requests.clone(); let async_context = ctx.context(); @@ -380,20 +380,10 @@ impl AsyncEngine, ManyOut, Error> tokio::select! { maybe_signal = request_rx.recv() => { let Some(signal) = maybe_signal else { - let _ = stream_tx.send(LLMEngineOutput::error("All output transmitters closed".to_string())).await; + let _ = stream_tx.send(LLMEngineOutput::error("All output transmitters closed".to_string())); break; }; - if signal.completed && token_count < max_tokens - 1 { - let _ = stream_tx.send(LLMEngineOutput::error("Completion signal received before max tokens reached".to_string())).await; - break; - } - - if signal.completed { - let _ = stream_tx.send(LLMEngineOutput::length()).await; - break; - } - // Generate a new token let token_id = generate_random_token(); token_count += 1; @@ -409,13 +399,25 @@ impl AsyncEngine, ManyOut, Error> index: None, }; - if stream_tx.send(output).await.is_err() { + if signal.completed && token_count < max_tokens { + let _ = stream_tx.send(LLMEngineOutput::error("Completion signal received before max tokens reached".to_string())); + break; + } + + if signal.completed { + let _ = stream_tx.send(output); + let _ = stream_tx.send(LLMEngineOutput::length()); + break; + } + + if stream_tx.send(output).is_err() { + tracing::error!("Output stream receiver closed."); break; } } _ = async_context.stopped() => { - let _ = stream_tx.send(LLMEngineOutput::cancelled()).await; + let _ = stream_tx.send(LLMEngineOutput::cancelled()); break; } } @@ -426,8 +428,8 @@ impl AsyncEngine, ManyOut, Error> active.remove(&request_uuid); }); - // Create a simple ReceiverStream which is naturally Send + Sync - let stream = ReceiverStream::new(stream_rx); + // Create a simple UnboundedReceiverStream which is naturally Send + Sync + let stream = UnboundedReceiverStream::new(stream_rx); Ok(ResponseStream::new(Box::pin(stream), ctx.context())) } } @@ -632,21 +634,20 @@ mod integration_tests { tracing::info!("✓ Router created"); // Create test requests for both DP workers - let create_request = |tokens: Vec, dp_rank: u32| PreprocessedRequest { - model: "mock".to_string(), - token_ids: tokens, - batch_token_ids: None, - stop_conditions: StopConditions { - max_tokens: Some(TOKENS_PER_REQUEST as u32), - ..Default::default() - }, - sampling_options: SamplingOptions::default(), - output_options: OutputOptions::default(), - eos_token_ids: vec![], - mdc_sum: None, - annotations: vec![format!("dp_rank:{dp_rank}")], - estimated_prefix_hit_num_blocks: None, - backend_instance_id: None, + let create_request = |tokens: Vec, dp_rank: u32| { + PreprocessedRequest::builder() + .model("mock".to_string()) + .token_ids(tokens) + .stop_conditions(StopConditions { + max_tokens: Some(TOKENS_PER_REQUEST as u32), + ..Default::default() + }) + .sampling_options(SamplingOptions::default()) + .output_options(OutputOptions::default()) + .eos_token_ids(vec![]) + .annotations(vec![format!("dp_rank:{dp_rank}")]) + .build() + .unwrap() }; let requests = vec![ diff --git a/lib/llm/src/protocols/common/preprocessor.rs b/lib/llm/src/protocols/common/preprocessor.rs index f163cc9e74..688b9356c8 100644 --- a/lib/llm/src/protocols/common/preprocessor.rs +++ b/lib/llm/src/protocols/common/preprocessor.rs @@ -5,6 +5,7 @@ use derive_builder::Builder; use serde::{Deserialize, Serialize}; use super::{OutputOptions, SamplingOptions, StopConditions}; +use crate::kv_router::RouterConfigOverride; use crate::protocols::TokenIdType; /// [`PreprocessedRequest`] is the internal representation of an LLM request. The [`dynamo.llm-preprocessor`] @@ -54,6 +55,10 @@ pub struct PreprocessedRequest { /// Targeted backend instance ID for the request #[builder(default)] pub backend_instance_id: Option, + + /// Router configuration overrides for this specific request + #[builder(default)] + pub router_config_override: Option, } impl PreprocessedRequest { diff --git a/tests/router/test_router_e2e_with_mockers.py b/tests/router/test_router_e2e_with_mockers.py index b029810e37..5912b42e28 100644 --- a/tests/router/test_router_e2e_with_mockers.py +++ b/tests/router/test_router_e2e_with_mockers.py @@ -11,7 +11,7 @@ import aiohttp import pytest -from dynamo._core import DistributedRuntime +from dynamo._core import DistributedRuntime, KvPushRouter, KvRouterConfig from tests.utils.managed_process import ManagedProcess pytestmark = pytest.mark.pre_merge @@ -104,7 +104,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): super().__exit__(exc_type, exc_val, exc_tb) -async def send_request_with_retry(url: str, payload: dict, max_retries: int = 4): +async def send_request_with_retry(url: str, payload: dict, max_retries: int = 8): """Send a single request with exponential backoff retry""" wait_time = 1 # Start with 1 second @@ -550,3 +550,243 @@ async def send_long_request(req_id, payload): if os.path.exists(mocker_args_file): os.unlink(mocker_args_file) + + +@pytest.mark.pre_merge +def test_kv_push_router_bindings(request, runtime_services): + """ + Test KvPushRouter Python bindings with mocker engines. + This test creates KvPushRouter as a Python object and verifies + token streaming with ignore_eos=True and max_tokens=20. + """ + + # runtime_services starts etcd and nats + logger.info("Starting KvPushRouter bindings test") + + # Create mocker args file + mocker_args = {"speedup_ratio": SPEEDUP_RATIO, "block_size": BLOCK_SIZE} + + mocker_args_file = os.path.join(request.node.name, "mocker_args.json") + with open(mocker_args_file, "w") as f: + json.dump(mocker_args, f) + + # Start mocker instances + mocker_processes = [] + + try: + # Start mockers + for i in range(NUM_MOCKERS): + # Use unique endpoints for each mocker + endpoint = "dyn://test-namespace.mocker.generate" + logger.info(f"Starting mocker instance {i} on endpoint {endpoint}") + + mocker = MockerProcess(request, endpoint, mocker_args_file) + mocker_processes.append(mocker) + + # Start all mockers + for mocker in mocker_processes: + mocker.__enter__() + + # Wait for mockers to be ready by sending a dummy request with retry + async def wait_for_mockers_ready(): + """Send a dummy request to ensure mockers are ready""" + runtime = get_runtime() + namespace = runtime.namespace("test-namespace") + component = namespace.component("mocker") + endpoint = component.endpoint("generate") + + kv_router_config = KvRouterConfig() + kv_push_router = KvPushRouter( + endpoint=endpoint, + block_size=BLOCK_SIZE, + kv_router_config=kv_router_config, + ) + + # Dummy request with minimal tokens + dummy_token_ids = [1, 2, 3] # Just a few tokens for testing + max_retries = 8 + wait_time = 1 + + for attempt in range(max_retries + 1): + try: + logger.info( + f"Sending dummy request to check mocker readiness (attempt {attempt + 1})" + ) + stream = await kv_push_router.generate( + token_ids=dummy_token_ids, + model=MODEL_NAME, + stop_conditions={"max_tokens": 1}, # Generate just 1 token + sampling_options={"temperature": 0.7}, + output_options={ + "include_input_tokens": False, + "return_full_text": False, + }, + ) + + # Consume the stream to verify it works + token_count = 0 + async for response in stream: + if isinstance(response, dict) and "token_ids" in response: + token_count += len(response["token_ids"]) + + logger.info( + f"Mockers are ready! Dummy request succeeded on attempt {attempt + 1}" + ) + return True + + except Exception as e: + logger.warning(f"Attempt {attempt + 1} failed with error: {e}") + if attempt < max_retries: + await asyncio.sleep(wait_time) + wait_time *= 2 # Exponential backoff + else: + raise RuntimeError( + f"Failed to connect to mockers after {max_retries + 1} attempts" + ) + + return False + + # Wait for mockers to be ready + asyncio.run(wait_for_mockers_ready()) + + # Run the async test + async def test_kv_push_router(): + # Get runtime and create endpoint + runtime = get_runtime() + namespace = runtime.namespace("test-namespace") + component = namespace.component("mocker") + endpoint = component.endpoint("generate") + + # Create KvRouterConfig with default settings + kv_router_config = KvRouterConfig() + + # Create KvPushRouter Python object + kv_push_router = KvPushRouter( + endpoint=endpoint, + block_size=BLOCK_SIZE, + kv_router_config=kv_router_config, + ) + + logger.info("Created KvPushRouter Python object") + + # Generate random token IDs (100 to 200 tokens) + num_input_tokens = random.randint(100, 200) + token_ids = [random.randint(1, 10000) for _ in range(num_input_tokens)] + + logger.info(f"Generated {num_input_tokens} random token IDs") + + # Set up generation parameters + stop_conditions = { + "ignore_eos": True, # Don't stop on EOS token + "max_tokens": 20, # Generate exactly 20 tokens + } + + sampling_options = {"temperature": 0.7, "top_p": 0.9} + + output_options = {"include_input_tokens": False, "return_full_text": False} + + # Test with router config overrides + router_config_override = { + "overlap_score_weight": 0.5, # Override the default weight + "router_temperature": 0.5, # Override the default temperature + } + + # Call generate method + logger.info( + "Calling generate method on KvPushRouter with router config overrides" + ) + logger.info(f"Router config overrides: {router_config_override}") + stream = await kv_push_router.generate( + token_ids=token_ids, + model=MODEL_NAME, + stop_conditions=stop_conditions, + sampling_options=sampling_options, + output_options=output_options, + router_config_override=router_config_override, + ) + + # Collect tokens from the SSE stream + generated_tokens = [] + async for response in stream: + if isinstance(response, dict): + # Check if response has token_ids + if "token_ids" in response: + tokens = response["token_ids"] + if isinstance(tokens, list): + generated_tokens.extend(tokens) + logger.debug(f"Received {len(tokens)} tokens: {tokens}") + + # Check for finish reason + if "finish_reason" in response: + logger.info( + f"Stream finished with reason: {response['finish_reason']}" + ) + + # Verify we got exactly 20 tokens + logger.info(f"Total generated tokens: {len(generated_tokens)}") + assert len(generated_tokens) == 20, ( + f"Expected exactly 20 tokens but got {len(generated_tokens)}. " + f"Tokens: {generated_tokens}" + ) + + logger.info( + "Successfully verified 20 tokens generated via KvPushRouter with overrides" + ) + + # Test again without overrides + logger.info("Testing again without router config overrides") + stream = await kv_push_router.generate( + token_ids=token_ids[:50], # Use fewer tokens for second test + model=MODEL_NAME, + stop_conditions={"max_tokens": 10}, + sampling_options=sampling_options, + output_options=output_options, + # No router_config_override this time + ) + + generated_tokens_no_override = [] + async for response in stream: + if isinstance(response, dict) and "token_ids" in response: + generated_tokens_no_override.extend(response["token_ids"]) + + assert ( + len(generated_tokens_no_override) == 10 + ), f"Expected 10 tokens but got {len(generated_tokens_no_override)}" + logger.info("Successfully verified generation without overrides") + + # Test with partial override (only temperature) + logger.info( + "Testing with partial router config override (temperature only)" + ) + partial_override = {"router_temperature": 0.1} + stream = await kv_push_router.generate( + token_ids=token_ids[:30], # Use even fewer tokens + model=MODEL_NAME, + stop_conditions={"max_tokens": 5}, + sampling_options=sampling_options, + output_options=output_options, + router_config_override=partial_override, + ) + + generated_tokens_partial = [] + async for response in stream: + if isinstance(response, dict) and "token_ids" in response: + generated_tokens_partial.extend(response["token_ids"]) + + assert ( + len(generated_tokens_partial) == 5 + ), f"Expected 5 tokens but got {len(generated_tokens_partial)}" + logger.info("Successfully verified generation with partial override") + + # Run the async test + asyncio.run(test_kv_push_router()) + + logger.info("KvPushRouter bindings test completed successfully") + + finally: + # Clean up mockers + for mocker in mocker_processes: + mocker.__exit__(None, None, None) + + if os.path.exists(mocker_args_file): + os.unlink(mocker_args_file) From 80f6b0b7abf984d6ba1af50e43641ec4570cb347 Mon Sep 17 00:00:00 2001 From: Keiven C <213854356+keivenchang@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:56:04 -0700 Subject: [PATCH 28/82] refactor: move uptime tracking from system_status_server(HTTP) to DRT level (#2587) Co-authored-by: Keiven Chang Signed-off-by: Jason Zhou --- lib/runtime/src/distributed.rs | 79 ++++++++++-- lib/runtime/src/lib.rs | 33 +++++ lib/runtime/src/metrics.rs | 6 +- lib/runtime/src/system_status_server.rs | 163 +++++++++--------------- 4 files changed, 165 insertions(+), 116 deletions(-) diff --git a/lib/runtime/src/distributed.rs b/lib/runtime/src/distributed.rs index 15559ec7ea..27e9c49a1f 100644 --- a/lib/runtime/src/distributed.rs +++ b/lib/runtime/src/distributed.rs @@ -117,6 +117,13 @@ impl DistributedRuntime { }); distributed_runtime.register_metrics_callback(drt_hierarchies, nats_client_callback); + // Initialize the uptime gauge in SystemHealth + distributed_runtime + .system_health + .lock() + .unwrap() + .initialize_uptime_gauge(&distributed_runtime)?; + // Handle system status server initialization if let Some(cancel_token) = cancel_token { // System server is enabled - start both the state and HTTP server @@ -153,17 +160,7 @@ impl DistributedRuntime { } } } else { - // System server HTTP is disabled, but still create the state for metrics - // This ensures uptime_seconds metric is always registered - let system_status_state = crate::system_status_server::SystemStatusState::new( - Arc::new(distributed_runtime.clone()), - )?; - - // Initialize the start time for uptime tracking - if let Err(e) = system_status_state.initialize_start_time() { - tracing::warn!("Failed to initialize system status start time: {}", e); - } - + // System server HTTP is disabled, but uptime metrics are still being tracked via SystemHealth tracing::debug!( "System status server HTTP endpoints disabled, but uptime metrics are being tracked" ); @@ -349,7 +346,7 @@ impl DistributedConfig { } #[cfg(test)] -pub mod test_helpers { +pub mod distributed_test_utils { //! Common test helper functions for DistributedRuntime tests // TODO: Use in-memory DistributedRuntime for tests instead of full runtime when available. @@ -364,3 +361,61 @@ pub mod test_helpers { .unwrap() } } + +#[cfg(feature = "integration")] +#[cfg(test)] +mod tests { + use super::distributed_test_utils::create_test_drt_async; + + #[tokio::test] + async fn test_drt_uptime_after_delay_system_disabled() { + // Test uptime with system status server disabled + temp_env::async_with_vars([("DYN_SYSTEM_ENABLED", Some("false"))], async { + // Start a DRT + let drt = create_test_drt_async().await; + + // Wait 50ms + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + + // Check that uptime is 50+ ms + let uptime = drt.system_health.lock().unwrap().uptime(); + assert!( + uptime >= std::time::Duration::from_millis(50), + "Expected uptime to be at least 50ms, but got {:?}", + uptime + ); + + println!( + "✓ DRT uptime test passed (system disabled): uptime = {:?}", + uptime + ); + }) + .await; + } + + #[tokio::test] + async fn test_drt_uptime_after_delay_system_enabled() { + // Test uptime with system status server enabled + temp_env::async_with_vars([("DYN_SYSTEM_ENABLED", Some("true"))], async { + // Start a DRT + let drt = create_test_drt_async().await; + + // Wait 50ms + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + + // Check that uptime is 50+ ms + let uptime = drt.system_health.lock().unwrap().uptime(); + assert!( + uptime >= std::time::Duration::from_millis(50), + "Expected uptime to be at least 50ms, but got {:?}", + uptime + ); + + println!( + "✓ DRT uptime test passed (system enabled): uptime = {:?}", + uptime + ); + }) + .await; + } +} diff --git a/lib/runtime/src/lib.rs b/lib/runtime/src/lib.rs index 5dc01ee7e1..6b3fb200ed 100644 --- a/lib/runtime/src/lib.rs +++ b/lib/runtime/src/lib.rs @@ -21,6 +21,7 @@ use std::{ collections::HashMap, sync::{Arc, OnceLock, Weak}, + time::Instant, }; use tokio::sync::Mutex; @@ -90,6 +91,8 @@ pub struct SystemHealth { use_endpoint_health_status: Vec, health_path: String, live_path: String, + start_time: Instant, + uptime_gauge: OnceLock, } impl SystemHealth { @@ -109,6 +112,8 @@ impl SystemHealth { use_endpoint_health_status, health_path, live_path, + start_time: Instant::now(), + uptime_gauge: OnceLock::new(), } } pub fn set_health_status(&mut self, status: HealthStatus) { @@ -145,6 +150,34 @@ impl SystemHealth { (healthy, endpoints) } + + /// Initialize the uptime gauge using the provided metrics registry + pub fn initialize_uptime_gauge( + &self, + registry: &T, + ) -> anyhow::Result<()> { + let gauge = registry.create_gauge( + "uptime_seconds", + "Total uptime of the DistributedRuntime in seconds", + &[], + )?; + self.uptime_gauge + .set(gauge) + .map_err(|_| anyhow::anyhow!("uptime_gauge already initialized"))?; + Ok(()) + } + + /// Get the current uptime as a Duration + pub fn uptime(&self) -> std::time::Duration { + self.start_time.elapsed() + } + + /// Update the uptime gauge with the current uptime value + pub fn update_uptime_gauge(&self) { + if let Some(gauge) = self.uptime_gauge.get() { + gauge.set(self.uptime().as_secs_f64()); + } + } } /// Type alias for runtime callback functions to reduce complexity diff --git a/lib/runtime/src/metrics.rs b/lib/runtime/src/metrics.rs index 58a690f854..55af79508f 100644 --- a/lib/runtime/src/metrics.rs +++ b/lib/runtime/src/metrics.rs @@ -913,7 +913,7 @@ mod test_metricsregistry_units { #[cfg(test)] mod test_metricsregistry_prefixes { use super::*; - use crate::distributed::test_helpers::create_test_drt_async; + use crate::distributed::distributed_test_utils::create_test_drt_async; use prometheus::core::Collector; #[tokio::test] @@ -1047,7 +1047,7 @@ mod test_metricsregistry_prometheus_fmt_outputs { use super::prometheus_names::{COMPONENT_NATS_METRICS, DRT_NATS_METRICS}; use super::prometheus_names::{nats_client, nats_service}; use super::*; - use crate::distributed::test_helpers::create_test_drt_async; + use crate::distributed::distributed_test_utils::create_test_drt_async; use prometheus::Counter; use std::sync::Arc; @@ -1308,7 +1308,7 @@ mod test_metricsregistry_nats { use super::prometheus_names::{COMPONENT_NATS_METRICS, DRT_NATS_METRICS}; use super::prometheus_names::{nats_client, nats_service}; use super::*; - use crate::distributed::test_helpers::create_test_drt_async; + use crate::distributed::distributed_test_utils::create_test_drt_async; use crate::pipeline::PushRouter; use crate::{DistributedRuntime, Runtime}; use tokio::time::{Duration, sleep}; diff --git a/lib/runtime/src/system_status_server.rs b/lib/runtime/src/system_status_server.rs index b6eca4f785..6bd0fa715b 100644 --- a/lib/runtime/src/system_status_server.rs +++ b/lib/runtime/src/system_status_server.rs @@ -19,6 +19,7 @@ use crate::metrics::MetricsRegistry; use crate::traits::DistributedRuntimeProvider; use axum::{Router, http::StatusCode, response::IntoResponse, routing::get}; use serde_json::json; +use std::collections::HashMap; use std::sync::{Arc, OnceLock}; use std::time::Instant; use tokio::{net::TcpListener, task::JoinHandle}; @@ -62,78 +63,22 @@ impl Clone for SystemStatusServerInfo { } } -/// System status server state containing metrics and uptime tracking +/// System status server state containing the distributed runtime reference pub struct SystemStatusState { // global drt registry is for printing out the entire Prometheus format output root_drt: Arc, - start_time: OnceLock, - uptime_gauge: prometheus::Gauge, } impl SystemStatusState { - /// Create new system status server state with the provided metrics registry + /// Create new system status server state with the provided distributed runtime pub fn new(drt: Arc) -> anyhow::Result { - // Note: This metric is created at the DRT level (no namespace), so it will be prefixed with "dynamo_component_" - // TODO(keiven): this is part of another upcoming refactor, where we will no longer - // have this duplicate DRT (and Duplicate metrics error). - let uptime_gauge = match drt.as_ref().create_gauge( - "uptime_seconds", - "Total uptime of the DistributedRuntime in seconds", - &[], - ) { - Ok(gauge) => gauge, - Err(e) if e.to_string().contains("Duplicate metrics") => { - // If the metric already exists, get it from the registry - // This can happen when SystemStatusState is created multiple times in tests - tracing::debug!( - "uptime_seconds metric already registered, retrieving existing metric" - ); - // Create a non-http gauge since we can't retrieve the existing one easily - // The important thing is that the metric is registered in the registry - prometheus::Gauge::new( - "uptime_seconds", - "Total uptime of the DistributedRuntime in seconds", - ) - .map_err(|e| anyhow::anyhow!("Failed to create dummy gauge: {}", e))? - } - Err(e) => return Err(e), - }; - let state = Self { - root_drt: drt, - start_time: OnceLock::new(), - uptime_gauge, - }; - Ok(state) - } - - /// Initialize the start time (can only be called once) - pub fn initialize_start_time(&self) -> Result<(), &'static str> { - self.start_time - .set(Instant::now()) - .map_err(|_| "Start time already initialized") - } - - pub fn uptime(&self) -> Result { - self.start_time - .get() - .ok_or("Start time not initialized") - .map(|start_time| start_time.elapsed()) + Ok(Self { root_drt: drt }) } /// Get a reference to the distributed runtime pub fn drt(&self) -> &crate::DistributedRuntime { &self.root_drt } - - /// Update the uptime gauge with current value - pub fn update_uptime_gauge(&self) { - if let Ok(uptime) = self.uptime() { - let uptime_seconds = uptime.as_secs_f64(); - self.uptime_gauge.set(uptime_seconds); - } else { - tracing::warn!("Failed to update uptime gauge: start time not initialized"); - } - } } /// Start system status server with metrics support @@ -143,7 +88,7 @@ pub async fn spawn_system_status_server( cancel_token: CancellationToken, drt: Arc, ) -> anyhow::Result<(std::net::SocketAddr, tokio::task::JoinHandle<()>)> { - // Create system status server state with the provided metrics registry + // Create system status server state with the provided distributed runtime let server_state = Arc::new(SystemStatusState::new(drt)?); let health_path = server_state .drt() @@ -160,11 +105,6 @@ pub async fn spawn_system_status_server( .live_path .clone(); - // Initialize the start time - server_state - .initialize_start_time() - .map_err(|e| anyhow::anyhow!("Failed to initialize start time: {}", e))?; - let app = Router::new() .route( &health_path, @@ -230,20 +170,9 @@ pub async fn spawn_system_status_server( /// Health handler #[tracing::instrument(skip_all, level = "trace")] async fn health_handler(state: Arc) -> impl IntoResponse { - let (mut healthy, endpoints) = state - .drt() - .system_health - .lock() - .unwrap() - .get_health_status(); - let uptime = match state.uptime() { - Ok(uptime_state) => Some(uptime_state), - Err(e) => { - tracing::error!("Failed to get uptime: {}", e); - healthy = false; - None - } - }; + let system_health = state.drt().system_health.lock().unwrap(); + let (healthy, endpoints) = system_health.get_health_status(); + let uptime = Some(system_health.uptime()); let healthy_string = if healthy { "ready" } else { "notready" }; let status_code = if healthy { @@ -267,7 +196,12 @@ async fn health_handler(state: Arc) -> impl IntoResponse { #[tracing::instrument(skip_all, level = "trace")] async fn metrics_handler(state: Arc) -> impl IntoResponse { // Update the uptime gauge with current value - state.update_uptime_gauge(); + state + .drt() + .system_health + .lock() + .unwrap() + .update_uptime_gauge(); // Execute all the callbacks starting at the DistributedRuntime level assert!(state.drt().basename() == ""); @@ -334,7 +268,7 @@ mod tests { #[cfg(all(test, feature = "integration"))] mod integration_tests { use super::*; - use crate::distributed::test_helpers::create_test_drt_async; + use crate::distributed::distributed_test_utils::create_test_drt_async; use crate::metrics::MetricsRegistry; use anyhow::Result; use rstest::rstest; @@ -342,16 +276,20 @@ mod integration_tests { use tokio::time::Duration; #[tokio::test] - async fn test_uptime_without_initialization() { - // Test that uptime returns an error if start time is not initialized + async fn test_uptime_from_system_health() { + // Test that uptime is available from SystemHealth temp_env::async_with_vars([("DYN_SYSTEM_ENABLED", Some("false"))], async { let drt = create_test_drt_async().await; - let system_status = SystemStatusState::new(Arc::new(drt)).unwrap(); - // This should return an error because start time is not initialized - let result = system_status.uptime(); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "Start time not initialized"); + // Get uptime from SystemHealth + let uptime = drt.system_health.lock().unwrap().uptime(); + // Uptime should exist (even if close to zero) + assert!(uptime.as_nanos() > 0 || uptime.is_zero()); + + // Sleep briefly and check uptime increases + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let uptime_after = drt.system_health.lock().unwrap().uptime(); + assert!(uptime_after > uptime); }) .await; } @@ -397,28 +335,51 @@ mod integration_tests { } #[tokio::test] - async fn test_start_time_initialization() { - // Test that start time can only be initialized once + async fn test_uptime_gauge_updates() { + // Test that the uptime gauge is properly updated and increases over time temp_env::async_with_vars([("DYN_SYSTEM_ENABLED", Some("false"))], async { let drt = create_test_drt_async().await; - let system_status = SystemStatusState::new(Arc::new(drt)).unwrap(); - // First initialization should succeed - assert!(system_status.initialize_start_time().is_ok()); + // Get initial uptime + let initial_uptime = drt.system_health.lock().unwrap().uptime(); - // Second initialization should fail - assert!(system_status.initialize_start_time().is_err()); + // Update the gauge with initial value + drt.system_health.lock().unwrap().update_uptime_gauge(); - // Sleep for 100ms and verify uptime increases + // Sleep for 100ms tokio::time::sleep(std::time::Duration::from_millis(100)).await; - let uptime_after_sleep = system_status.uptime().unwrap(); + + // Get uptime after sleep + let uptime_after_sleep = drt.system_health.lock().unwrap().uptime(); + + // Update the gauge again + drt.system_health.lock().unwrap().update_uptime_gauge(); + + // Verify uptime increased by at least 100ms + let elapsed = uptime_after_sleep - initial_uptime; + assert!( + elapsed >= std::time::Duration::from_millis(100), + "Uptime should have increased by at least 100ms after sleep, but only increased by {:?}", + elapsed + ); + }) + .await; + } + + #[tokio::test] + async fn test_http_requests_fail_when_system_disabled() { + // Test that system status server is not running when disabled + temp_env::async_with_vars([("DYN_SYSTEM_ENABLED", Some("false"))], async { + let drt = create_test_drt_async().await; + + // Verify that system status server info is None when disabled + let system_info = drt.system_status_server_info(); assert!( - uptime_after_sleep >= std::time::Duration::from_millis(100), - "Uptime should be at least 100ms after sleep, got: {:?}", - uptime_after_sleep + system_info.is_none(), + "System status server should not be running when DYN_SYSTEM_ENABLED=false" ); - // If we get here, uptime calculation works correctly + println!("✓ System status server correctly disabled when DYN_SYSTEM_ENABLED=false"); }) .await; } From 1f8699998e0449d579c76063ea6182c7492e0943 Mon Sep 17 00:00:00 2001 From: nachiketb-nvidia Date: Mon, 25 Aug 2025 15:57:25 -0700 Subject: [PATCH 29/82] feat: enable --dyn-reasoning-parser flag to set reasoning parser for vllm deployments (#2700) Signed-off-by: Jason Zhou --- .../backends/vllm/src/dynamo/vllm/args.py | 2 +- lib/llm/src/engines.rs | 3 +- lib/llm/src/local_model.rs | 2 ++ lib/llm/src/model_card.rs | 6 ++++ lib/llm/src/preprocessor.rs | 7 ++++- .../openai/chat_completions/delta.rs | 19 +++++++++--- lib/llm/tests/http-service.rs | 29 +++++++++++-------- lib/parsers/src/reasoning/mod.rs | 16 ++++++++++ 8 files changed, 65 insertions(+), 19 deletions(-) diff --git a/components/backends/vllm/src/dynamo/vllm/args.py b/components/backends/vllm/src/dynamo/vllm/args.py index 381fcb38fc..ca339b7b15 100644 --- a/components/backends/vllm/src/dynamo/vllm/args.py +++ b/components/backends/vllm/src/dynamo/vllm/args.py @@ -117,7 +117,7 @@ def parse_args() -> Config: "--dyn-reasoning-parser", type=str, default=None, - help="Reasoning parser name for the model.", + help="Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss'.", ) parser = AsyncEngineArgs.add_cli_args(parser) diff --git a/lib/llm/src/engines.rs b/lib/llm/src/engines.rs index c60956c892..43fc3002fa 100644 --- a/lib/llm/src/engines.rs +++ b/lib/llm/src/engines.rs @@ -14,6 +14,7 @@ use dynamo_runtime::pipeline::{Error, ManyOut, SingleIn}; use dynamo_runtime::protocols::annotated::Annotated; use crate::backend::ExecutionContext; +use crate::local_model::runtime_config; use crate::preprocessor::PreprocessedRequest; use crate::protocols::common::llm_backend::LLMEngineOutput; use crate::protocols::openai::{ @@ -183,7 +184,7 @@ impl incoming_request: SingleIn, ) -> Result>, Error> { let (request, context) = incoming_request.transfer(()); - let mut deltas = request.response_generator(); + let mut deltas = request.response_generator(runtime_config::ModelRuntimeConfig::default()); let ctx = context.context(); let req = request.inner.messages.into_iter().next_back().unwrap(); diff --git a/lib/llm/src/local_model.rs b/lib/llm/src/local_model.rs index fb5d1bbc45..eb5899359a 100644 --- a/lib/llm/src/local_model.rs +++ b/lib/llm/src/local_model.rs @@ -202,6 +202,7 @@ impl LocalModelBuilder { ); card.migration_limit = self.migration_limit; card.user_data = self.user_data.take(); + card.runtime_config = self.runtime_config.clone(); return Ok(LocalModel { card, @@ -276,6 +277,7 @@ impl LocalModelBuilder { card.migration_limit = self.migration_limit; card.user_data = self.user_data.take(); + card.runtime_config = self.runtime_config.clone(); Ok(LocalModel { card, diff --git a/lib/llm/src/model_card.rs b/lib/llm/src/model_card.rs index e4445c5512..f389e6eea0 100644 --- a/lib/llm/src/model_card.rs +++ b/lib/llm/src/model_card.rs @@ -19,6 +19,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use crate::local_model::runtime_config::ModelRuntimeConfig; use anyhow::{Context, Result}; use derive_builder::Builder; use dynamo_runtime::{slug::Slug, storage::key_value_store::Versioned, transports::nats}; @@ -137,6 +138,9 @@ pub struct ModelDeploymentCard { /// User-defined metadata for custom worker behavior #[serde(default, skip_serializing_if = "Option::is_none")] pub user_data: Option, + + #[serde(default)] + pub runtime_config: ModelRuntimeConfig, } impl ModelDeploymentCard { @@ -441,6 +445,7 @@ impl ModelDeploymentCard { kv_cache_block_size: 0, migration_limit: 0, user_data: None, + runtime_config: ModelRuntimeConfig::default(), }) } @@ -482,6 +487,7 @@ impl ModelDeploymentCard { kv_cache_block_size: 0, // set later migration_limit: 0, user_data: None, + runtime_config: ModelRuntimeConfig::default(), }) } } diff --git a/lib/llm/src/preprocessor.rs b/lib/llm/src/preprocessor.rs index 076e5c1dfa..7c96f49a22 100644 --- a/lib/llm/src/preprocessor.rs +++ b/lib/llm/src/preprocessor.rs @@ -22,6 +22,7 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use std::{collections::HashMap, sync::Arc}; use tracing; +use crate::local_model::runtime_config::ModelRuntimeConfig; use crate::model_card::{ModelDeploymentCard, ModelInfo, TokenizerKind}; use crate::preprocessor::prompt::OAIChatLikeRequest; use crate::tokenizers::Encoding; @@ -94,6 +95,7 @@ pub struct OpenAIPreprocessor { formatter: Arc, tokenizer: Arc, model_info: Arc, + runtime_config: ModelRuntimeConfig, } impl OpenAIPreprocessor { @@ -121,11 +123,14 @@ impl OpenAIPreprocessor { }; let model_info = model_info.get_model_info().await?; + let runtime_config = mdc.runtime_config.clone(); + Ok(Arc::new(Self { formatter, tokenizer, model_info, mdcsum, + runtime_config, })) } @@ -494,7 +499,7 @@ impl let (request, context) = request.into_parts(); // create a response generator - let response_generator = request.response_generator(); + let response_generator = request.response_generator(self.runtime_config.clone()); let mut response_generator = Box::new(response_generator); // convert the chat completion request to a common completion request diff --git a/lib/llm/src/protocols/openai/chat_completions/delta.rs b/lib/llm/src/protocols/openai/chat_completions/delta.rs index e023f54300..b64a8019bc 100644 --- a/lib/llm/src/protocols/openai/chat_completions/delta.rs +++ b/lib/llm/src/protocols/openai/chat_completions/delta.rs @@ -5,6 +5,7 @@ use dynamo_parsers::{ParserResult, ReasoningParser, ReasoningParserType, Reasoni use super::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse}; use crate::{ + local_model::runtime_config, protocols::common::{self}, types::TokenIdType, }; @@ -15,11 +16,15 @@ impl NvCreateChatCompletionRequest { /// /// # Returns /// * [`DeltaGenerator`] configured with model name and response options. - pub fn response_generator(&self) -> DeltaGenerator { + pub fn response_generator( + &self, + runtime_config: runtime_config::ModelRuntimeConfig, + ) -> DeltaGenerator { let options = DeltaGeneratorOptions { enable_usage: true, enable_logprobs: self.inner.logprobs.unwrap_or(false) || self.inner.top_logprobs.unwrap_or(0) > 0, + runtime_config, }; DeltaGenerator::new(self.inner.model.clone(), options) @@ -33,6 +38,8 @@ pub struct DeltaGeneratorOptions { pub enable_usage: bool, /// Determines whether log probabilities should be included in the response. pub enable_logprobs: bool, + + pub runtime_config: runtime_config::ModelRuntimeConfig, } /// Generates incremental chat completion responses in a streaming fashion. @@ -92,10 +99,14 @@ impl DeltaGenerator { // This is hardcoded for now, but can be made configurable later. // TODO: Make parser type configurable once front-end integration is determined // Change to GptOss to test GptOSS parser - let reasoning_parser_type = ReasoningParserType::Basic; - // Reasoning parser wrapper - let reasoning_parser = reasoning_parser_type.get_reasoning_parser(); + let reasoning_parser = ReasoningParserType::get_reasoning_parser_from_name( + options + .runtime_config + .reasoning_parser + .as_deref() + .unwrap_or("basic"), + ); Self { id: format!("chatcmpl-{}", uuid::Uuid::new_v4()), diff --git a/lib/llm/tests/http-service.rs b/lib/llm/tests/http-service.rs index 4e70ae0796..7bbaad2247 100644 --- a/lib/llm/tests/http-service.rs +++ b/lib/llm/tests/http-service.rs @@ -16,17 +16,6 @@ use anyhow::Error; use async_stream::stream; use dynamo_async_openai::config::OpenAIConfig; -use dynamo_llm::http::{ - client::{ - GenericBYOTClient, HttpClientConfig, HttpRequestContext, NvCustomClient, PureOpenAIClient, - }, - service::{ - Metrics, - error::HttpError, - metrics::{Endpoint, FRONTEND_METRIC_PREFIX, RequestType, Status}, - service_v2::HttpService, - }, -}; use dynamo_llm::protocols::{ Annotated, codec::SseLineCodec, @@ -36,6 +25,21 @@ use dynamo_llm::protocols::{ completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, }, }; +use dynamo_llm::{ + http::{ + client::{ + GenericBYOTClient, HttpClientConfig, HttpRequestContext, NvCustomClient, + PureOpenAIClient, + }, + service::{ + Metrics, + error::HttpError, + metrics::{Endpoint, FRONTEND_METRIC_PREFIX, RequestType, Status}, + service_v2::HttpService, + }, + }, + local_model::runtime_config, +}; use dynamo_runtime::{ CancellationToken, engine::AsyncEngineContext, @@ -95,7 +99,8 @@ impl let max_tokens = request.inner.max_tokens.unwrap_or(0) as u64; // let generator = NvCreateChatCompletionStreamResponse::generator(request.model.clone()); - let mut generator = request.response_generator(); + let mut generator = + request.response_generator(runtime_config::ModelRuntimeConfig::default()); let stream = stream! { tokio::time::sleep(std::time::Duration::from_millis(max_tokens)).await; diff --git a/lib/parsers/src/reasoning/mod.rs b/lib/parsers/src/reasoning/mod.rs index 070d3656b5..f329acb799 100644 --- a/lib/parsers/src/reasoning/mod.rs +++ b/lib/parsers/src/reasoning/mod.rs @@ -115,4 +115,20 @@ impl ReasoningParserType { }, } } + + pub fn get_reasoning_parser_from_name(name: &str) -> ReasoningParserWrapper { + tracing::debug!("Selected reasoning parser: {}", name); + match name.to_lowercase().as_str() { + "deepseek_r1" => Self::DeepseekR1.get_reasoning_parser(), + "basic" => Self::Basic.get_reasoning_parser(), + "gpt_oss" => Self::GptOss.get_reasoning_parser(), + _ => { + tracing::warn!( + "Unknown reasoning parser type '{}', falling back to Basic Reasoning Parser", + name + ); + Self::Basic.get_reasoning_parser() + } + } + } } From 5fad214932f6ba1b78a1cde2c384b402a6b8a968 Mon Sep 17 00:00:00 2001 From: Ryan McCormick Date: Mon, 25 Aug 2025 16:36:31 -0700 Subject: [PATCH 30/82] docs: Simplify sphinx build and table of contents on webpage (#2519) Signed-off-by: Jason Zhou --- components/backends/sglang/deploy/README.md | 4 +- components/backends/trtllm/deploy/README.md | 6 +- components/backends/vllm/deploy/README.md | 6 +- deploy/inference-gateway/README.md | 2 +- docs/_includes/dive_in_examples.rst | 32 +++ docs/_includes/install.rst | 44 +++ docs/_includes/quick_start_local.rst | 43 +++ docs/_sections/architecture.rst | 11 + docs/_sections/backends.rst | 42 +++ docs/_sections/examples.rst | 8 + docs/_sections/installation.rst | 10 + docs/architecture/kvbm_intro.rst | 5 +- docs/architecture/planner_intro.rst | 8 +- docs/architecture/pre_deployment_profiling.md | 2 +- docs/components/backends/llm/README.md | 1 - docs/components/backends/sglang/README.md | 1 + .../trtllm/multinode/multinode-examples.md | 1 + .../backends/vllm/LMCache_Integration.md | 1 + docs/conf.py | 252 +++--------------- docs/examples/README.md | 93 +------ docs/guides/backend.md | 2 +- docs/guides/dynamo_deploy/dynamo_cloud.md | 8 +- docs/guides/dynamo_deploy/dynamo_operator.md | 2 +- .../dynamo_deploy/operator_deployment.md | 1 - docs/guides/dynamo_deploy/quickstart.md | 196 -------------- docs/hidden_toctree.rst | 50 ++-- docs/index.rst | 163 +++-------- examples/basics/multinode/README.md | 1 + examples/runtime/hello_world/README.md | 4 +- 29 files changed, 303 insertions(+), 696 deletions(-) create mode 100644 docs/_includes/dive_in_examples.rst create mode 100644 docs/_includes/install.rst create mode 100644 docs/_includes/quick_start_local.rst create mode 100644 docs/_sections/architecture.rst create mode 100644 docs/_sections/backends.rst create mode 100644 docs/_sections/examples.rst create mode 100644 docs/_sections/installation.rst delete mode 120000 docs/components/backends/llm/README.md create mode 120000 docs/components/backends/sglang/README.md create mode 120000 docs/components/backends/trtllm/multinode/multinode-examples.md create mode 120000 docs/components/backends/vllm/LMCache_Integration.md mode change 100755 => 100644 docs/conf.py mode change 100644 => 120000 docs/examples/README.md delete mode 120000 docs/guides/dynamo_deploy/operator_deployment.md delete mode 100644 docs/guides/dynamo_deploy/quickstart.md diff --git a/components/backends/sglang/deploy/README.md b/components/backends/sglang/deploy/README.md index 4929eeb971..cd7715bc81 100644 --- a/components/backends/sglang/deploy/README.md +++ b/components/backends/sglang/deploy/README.md @@ -145,7 +145,7 @@ All templates use **DeepSeek-R1-Distill-Llama-8B** as the default model. But you ## Further Reading - **Deployment Guide**: [Creating Kubernetes Deployments](../../../../docs/guides/dynamo_deploy/create_deployment.md) -- **Quickstart**: [Deployment Quickstart](../../../../docs/guides/dynamo_deploy/quickstart.md) +- **Quickstart**: [Deployment Quickstart](../../../../docs/guides/dynamo_deploy/README.md) - **Platform Setup**: [Dynamo Cloud Installation](../../../../docs/guides/dynamo_deploy/dynamo_cloud.md) - **Examples**: [Deployment Examples](../../../../docs/examples/README.md) - **Kubernetes CRDs**: [Custom Resources Documentation](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) @@ -159,4 +159,4 @@ Common issues and solutions: 3. **Health check failures**: Review model loading logs and increase `initialDelaySeconds` 4. **Out of memory**: Increase memory limits or reduce model batch size -For additional support, refer to the [deployment guide](../../../../docs/guides/dynamo_deploy/quickstart.md). +For additional support, refer to the [deployment guide](../../../../docs/guides/dynamo_deploy/README.md). diff --git a/components/backends/trtllm/deploy/README.md b/components/backends/trtllm/deploy/README.md index a02b7afe62..cde9bd02bc 100644 --- a/components/backends/trtllm/deploy/README.md +++ b/components/backends/trtllm/deploy/README.md @@ -81,7 +81,7 @@ extraPodSpec: Before using these templates, ensure you have: -1. **Dynamo Cloud Platform installed** - See [Quickstart Guide](../../../../docs/guides/dynamo_deploy/quickstart.md) +1. **Dynamo Cloud Platform installed** - See [Quickstart Guide](../../../../docs/guides/dynamo_deploy/README.md) 2. **Kubernetes cluster with GPU support** 3. **Container registry access** for TensorRT-LLM runtime images 4. **HuggingFace token secret** (referenced as `envFromSecret: hf-token-secret`) @@ -257,7 +257,7 @@ Configure the `model` name and `host` based on your deployment. ## Further Reading - **Deployment Guide**: [Creating Kubernetes Deployments](../../../../docs/guides/dynamo_deploy/create_deployment.md) -- **Quickstart**: [Deployment Quickstart](../../../../docs/guides/dynamo_deploy/quickstart.md) +- **Quickstart**: [Deployment Quickstart](../../../../docs/guides/dynamo_deploy/README.md) - **Platform Setup**: [Dynamo Cloud Installation](../../../../docs/guides/dynamo_deploy/dynamo_cloud.md) - **Examples**: [Deployment Examples](../../../../docs/examples/README.md) - **Architecture Docs**: [Disaggregated Serving](../../../../docs/architecture/disagg_serving.md), [KV-Aware Routing](../../../../docs/architecture/kv_cache_routing.md) @@ -277,4 +277,4 @@ Common issues and solutions: 6. **Git LFS issues**: Ensure git-lfs is installed before building containers 7. **ARM deployment**: Use `--platform linux/arm64` when building on ARM machines -For additional support, refer to the [deployment troubleshooting guide](../../../../docs/guides/dynamo_deploy/quickstart.md#troubleshooting). +For additional support, refer to the [deployment troubleshooting guide](../../../../docs/guides/dynamo_deploy/README.md). diff --git a/components/backends/vllm/deploy/README.md b/components/backends/vllm/deploy/README.md index a720036a90..db43a7801f 100644 --- a/components/backends/vllm/deploy/README.md +++ b/components/backends/vllm/deploy/README.md @@ -82,7 +82,7 @@ extraPodSpec: Before using these templates, ensure you have: -1. **Dynamo Cloud Platform installed** - See [Quickstart Guide](../../../../docs/guides/dynamo_deploy/quickstart.md) +1. **Dynamo Cloud Platform installed** - See [Quickstart Guide](../../../../docs/guides/dynamo_deploy/README.md) 2. **Kubernetes cluster with GPU support** 3. **Container registry access** for vLLM runtime images 4. **HuggingFace token secret** (referenced as `envFromSecret: hf-token-secret`) @@ -236,7 +236,7 @@ args: ## Further Reading - **Deployment Guide**: [Creating Kubernetes Deployments](../../../../docs/guides/dynamo_deploy/create_deployment.md) -- **Quickstart**: [Deployment Quickstart](../../../../docs/guides/dynamo_deploy/quickstart.md) +- **Quickstart**: [Deployment Quickstart](../../../../docs/guides/dynamo_deploy/README.md) - **Platform Setup**: [Dynamo Cloud Installation](../../../../docs/guides/dynamo_deploy/dynamo_cloud.md) - **SLA Planner**: [SLA Planner Deployment Guide](../../../../docs/guides/dynamo_deploy/sla_planner_deployment.md) - **Examples**: [Deployment Examples](../../../../docs/examples/README.md) @@ -252,4 +252,4 @@ Common issues and solutions: 4. **Out of memory**: Increase memory limits or reduce model batch size 5. **Port forwarding issues**: Ensure correct pod UUID in port-forward command -For additional support, refer to the [deployment troubleshooting guide](../../../../docs/guides/dynamo_deploy/quickstart.md#troubleshooting). \ No newline at end of file +For additional support, refer to the [deployment troubleshooting guide](../../../../docs/guides/dynamo_deploy/README.md). diff --git a/deploy/inference-gateway/README.md b/deploy/inference-gateway/README.md index 2ef635c946..ada2af2293 100644 --- a/deploy/inference-gateway/README.md +++ b/deploy/inference-gateway/README.md @@ -20,7 +20,7 @@ Currently, these setups are only supported with the kGateway based Inference Gat 1. **Install Dynamo Platform** -[See Quickstart Guide](../../docs/guides/dynamo_deploy/quickstart.md) to install Dynamo Cloud. +[See Quickstart Guide](../../docs/guides/dynamo_deploy/README.md) to install Dynamo Cloud. 2. **Deploy Inference Gateway** diff --git a/docs/_includes/dive_in_examples.rst b/docs/_includes/dive_in_examples.rst new file mode 100644 index 0000000000..60eb9048fb --- /dev/null +++ b/docs/_includes/dive_in_examples.rst @@ -0,0 +1,32 @@ +The examples below assume you build the latest image yourself from source. If using a prebuilt image follow the examples from the corresponding branch. + +.. grid:: 1 2 2 2 + :gutter: 3 + :margin: 0 + :padding: 3 4 0 0 + + .. grid-item-card:: :doc:`Hello World <../examples/runtime/hello_world/README>` + :link: ../examples/runtime/hello_world/README + :link-type: doc + + Demonstrates the basic concepts of Dynamo by creating a simple GPU-unaware graph + + .. grid-item-card:: :doc:`vLLM <../components/backends/vllm/README>` + :link: ../components/backends/vllm/README + :link-type: doc + + Presents examples and reference implementations for deploying Large Language Models (LLMs) in various configurations with VLLM. + + .. grid-item-card:: :doc:`SGLang <../components/backends/sglang/README>` + :link: ../components/backends/sglang/README + :link-type: doc + + Presents examples and reference implementations for deploying Large Language Models (LLMs) in various configurations with SGLang. + + .. grid-item-card:: :doc:`TensorRT-LLM <../components/backends/trtllm/README>` + :link: ../components/backends/trtllm/README + :link-type: doc + + Presents examples and reference implementations for deploying Large Language Models (LLMs) in various configurations with TensorRT-LLM. + + diff --git a/docs/_includes/install.rst b/docs/_includes/install.rst new file mode 100644 index 0000000000..7d7309763f --- /dev/null +++ b/docs/_includes/install.rst @@ -0,0 +1,44 @@ +Pip (PyPI) +---------- + +Install a pre-built wheel from PyPI. + +.. code-block:: bash + + # Create a virtual environment and activate it + uv venv venv + source venv/bin/activate + + # Install Dynamo from PyPI (choose one backend extra) + uv pip install "ai-dynamo[sglang]==0.4.1" # or [vllm], [trtllm] + + +Pip from source +--------------- + +Install directly from a local checkout for development. + +.. code-block:: bash + + # Clone the repository + git clone https://github.com/ai-dynamo/dynamo.git + cd dynamo + + # Create a virtual environment and activate it + uv venv venv + source venv/bin/activate + uv pip install ".[sglang]" # or [vllm], [trtllm] + + +Docker +------ + +Pull and run prebuilt images from NVIDIA NGC (`nvcr.io`). + +.. code-block:: bash + + # Run a container (mount your workspace if needed) + docker run --rm -it \ + --gpus all \ + --network host \ + nvcr.io/nvidia/ai-dynamo/sglang-runtime:0.4.1 # or vllm, tensorrtllm diff --git a/docs/_includes/quick_start_local.rst b/docs/_includes/quick_start_local.rst new file mode 100644 index 0000000000..8d74d3d2ba --- /dev/null +++ b/docs/_includes/quick_start_local.rst @@ -0,0 +1,43 @@ +Get started with Dynamo locally in just a few commands: + +**1. Install Dynamo** + +.. code-block:: bash + + # Install uv (recommended Python package manager) + curl -LsSf https://astral.sh/uv/install.sh | sh + + # Create virtual environment and install Dynamo + uv venv venv + source venv/bin/activate + uv pip install "ai-dynamo[sglang]==0.4.1" # or [vllm], [trtllm] + +**2. Start etcd/NATS** + +.. code-block:: bash + + # Fetch and start etcd and NATS using Docker Compose + curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/ai-dynamo/dynamo/release/0.4.1/deploy/docker-compose.yml + docker compose -f docker-compose.yml up -d + +**3. Run Dynamo** + +.. code-block:: bash + + # Start the OpenAI compatible frontend (default port is 8080) + python -m dynamo.frontend + + # In another terminal, start an SGLang worker + python -m dynamo.sglang --model-path Qwen/Qwen3-0.6B + +**4. Test your deployment** + +.. code-block:: bash + + curl localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "Qwen/Qwen3-0.6B", + "messages": [{"role": "user", "content": "Hello!"}], + "max_tokens": 50}' + + diff --git a/docs/_sections/architecture.rst b/docs/_sections/architecture.rst new file mode 100644 index 0000000000..13e6dad0a5 --- /dev/null +++ b/docs/_sections/architecture.rst @@ -0,0 +1,11 @@ +Overview +============ + +.. include:: ../architecture/architecture.md + :parser: myst_parser.sphinx_ + +.. toctree:: + :hidden: + + Overview + Disaggregated Serving <../architecture/disagg_serving> diff --git a/docs/_sections/backends.rst b/docs/_sections/backends.rst new file mode 100644 index 0000000000..4b6b294b71 --- /dev/null +++ b/docs/_sections/backends.rst @@ -0,0 +1,42 @@ +.. + SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + + Licensed 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. + +Backends +======== + +NVIDIA Dynamo supports multiple inference backends to provide flexibility and performance optimization for different use cases and model architectures. Backends are the underlying engines that execute AI model inference, each optimized for specific scenarios, hardware configurations, and performance requirements. + +Overview +-------- + +Dynamo's multi-backend architecture allows you to: + +* **Choose the optimal engine** for your specific workload and hardware +* **Switch between backends** without changing your application code +* **Leverage specialized optimizations** from each backend +* **Scale flexibly** across different deployment scenarios + +Supported Backends +------------------ + +Dynamo currently supports the following high-performance inference backends: + +.. toctree:: + :maxdepth: 1 + + vLLM <../components/backends/vllm/README> + SGLang <../components/backends/sglang/README> + TensorRT-LLM <../components/backends/trtllm/README> diff --git a/docs/_sections/examples.rst b/docs/_sections/examples.rst new file mode 100644 index 0000000000..30258a46be --- /dev/null +++ b/docs/_sections/examples.rst @@ -0,0 +1,8 @@ +.. + Quickstart Page (left sidebar target) +.. + +Examples +======== + +.. include:: ../_includes/dive_in_examples.rst \ No newline at end of file diff --git a/docs/_sections/installation.rst b/docs/_sections/installation.rst new file mode 100644 index 0000000000..b9543fb558 --- /dev/null +++ b/docs/_sections/installation.rst @@ -0,0 +1,10 @@ +.. + Installation Page (left sidebar target) +.. + +Installation +============ + +.. include:: ../_includes/install.rst + + diff --git a/docs/architecture/kvbm_intro.rst b/docs/architecture/kvbm_intro.rst index 39b096a5c0..4c6cb0d227 100644 --- a/docs/architecture/kvbm_intro.rst +++ b/docs/architecture/kvbm_intro.rst @@ -48,9 +48,6 @@ The Dynamo KV Block Manager serves as a reference implementation that emphasizes * - - ❌ - SGLang - * - - - ❌ - - llama.cpp * - **Serving Type** - ✅ - Aggregated @@ -61,7 +58,9 @@ The Dynamo KV Block Manager serves as a reference implementation that emphasizes .. toctree:: :hidden: + Overview Motivation KVBM Architecture Understanding KVBM components KVBM Further Reading + LMCache Integration <../components/backends/vllm/LMCache_Integration.md> diff --git a/docs/architecture/planner_intro.rst b/docs/architecture/planner_intro.rst index e9c2e1eaf4..8c8dbccb5a 100644 --- a/docs/architecture/planner_intro.rst +++ b/docs/architecture/planner_intro.rst @@ -49,9 +49,6 @@ Key features include: * - - ❌ - SGLang - * - - - ❌ - - llama.cpp * - **Serving Type** - ✅ - Aggregated @@ -73,6 +70,7 @@ Key features include: .. toctree:: :hidden: + Overview Pre-Deployment Profiling - Load-based Planner - SLA-based Planner \ No newline at end of file + SLA-based Planner + Planner Benchmark <../guides/planner_benchmark/README.md> \ No newline at end of file diff --git a/docs/architecture/pre_deployment_profiling.md b/docs/architecture/pre_deployment_profiling.md index e76fc985f5..875c2353da 100644 --- a/docs/architecture/pre_deployment_profiling.md +++ b/docs/architecture/pre_deployment_profiling.md @@ -96,7 +96,7 @@ Use the default pre-built image and inject custom configurations via PVC: 1. **Set the container image:** ```bash - export DOCKER_IMAGE=nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.4.0 # or any existing image tag + export DOCKER_IMAGE=nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.4.1 # or any existing image tag ``` 2. **Inject your custom disagg configuration:** diff --git a/docs/components/backends/llm/README.md b/docs/components/backends/llm/README.md deleted file mode 120000 index 615da9417b..0000000000 --- a/docs/components/backends/llm/README.md +++ /dev/null @@ -1 +0,0 @@ -../../../../components/backends/llm/README.md \ No newline at end of file diff --git a/docs/components/backends/sglang/README.md b/docs/components/backends/sglang/README.md new file mode 120000 index 0000000000..c481015d87 --- /dev/null +++ b/docs/components/backends/sglang/README.md @@ -0,0 +1 @@ +../../../../components/backends/sglang/README.md \ No newline at end of file diff --git a/docs/components/backends/trtllm/multinode/multinode-examples.md b/docs/components/backends/trtllm/multinode/multinode-examples.md new file mode 120000 index 0000000000..495f44690b --- /dev/null +++ b/docs/components/backends/trtllm/multinode/multinode-examples.md @@ -0,0 +1 @@ +../../../../../components/backends/trtllm/multinode/multinode-examples.md \ No newline at end of file diff --git a/docs/components/backends/vllm/LMCache_Integration.md b/docs/components/backends/vllm/LMCache_Integration.md new file mode 120000 index 0000000000..117bf4be15 --- /dev/null +++ b/docs/components/backends/vllm/LMCache_Integration.md @@ -0,0 +1 @@ +../../../../components/backends/vllm/LMCache_Integration.md \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py old mode 100755 new mode 100644 index 3c10e46c2e..546b8c3ad0 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,71 +1,18 @@ -#!/usr/bin/env python3 - # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. # Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -import json import os import sys -from datetime import date - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import httplib2 -from packaging.version import Version - -sys.path.insert(0, os.path.abspath("_extensions")) - -# -- conf.py setup ----------------------------------------------------------- - -# conf.py needs to be run in the top level 'docs' -# directory but the calling build script needs to -# be called from the current working directory. We -# change to the 'docs' dir here and then revert back -# at the end of the file. -# current_dir = os.getcwd() -# os.chdir("docs") # -- Project information ----------------------------------------------------- - -project = "Dynamo" -copyright = "2025-{}, NVIDIA Corporation".format(date.today().year) +project = "NVIDIA Dynamo" +copyright = "2024-2025, NVIDIA CORPORATION & AFFILIATES" author = "NVIDIA" -# Get the version of dynamo this is building. -version_long = "0.1.0" - -version_short = version_long -version_short_split = version_short.split(".") -one_before = f"{version_short_split[0]}.{int(version_short_split[1]) - 1}.{version_short_split[2]}" - - # -- General configuration --------------------------------------------------- -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. +# Standard extensions extensions = [ "ablog", "myst_parser", @@ -82,188 +29,53 @@ "sphinx.ext.ifconfig", "sphinx.ext.extlinks", "sphinxcontrib.mermaid", - "github_alerts", # Custom extension for GitHub alert conversion -] - -suppress_warnings = ["myst.domains", "ref.ref", "myst.header"] - -source_suffix = [".rst", ".md"] - -autodoc_default_options = { - "members": True, - "undoc-members": True, - "private-members": True, -} - -autosummary_generate = True -autosummary_mock_imports = [ - "tritonclient.grpc.model_config_pb2", - "tritonclient.grpc.service_pb2", - "tritonclient.grpc.service_pb2_grpc", ] -napoleon_include_special_with_doc = True +# Custom extensions +sys.path.insert(0, os.path.abspath("_extensions")) +extensions.append("github_alerts") -numfig = True +# Handle Mermaid diagrams as code blocks (not directives) to avoid warnings +myst_fence_as_directive = ["mermaid"] # Uncomment if sphinxcontrib-mermaid is installed -# final location of docs for seo/sitemap -html_baseurl = "https://docs.nvidia.com/dynamo/latest/" +# File extensions (myst_parser automatically handles .md files) +source_suffix = [".rst", ".md"] +# MyST parser configuration myst_enable_extensions = [ - "dollarmath", - "amsmath", - "deflist", - # "html_admonition", - "html_image", - "colon_fence", - # "smartquotes", - "replacements", - # "linkify", - "substitution", + "colon_fence", # ::: code blocks + "deflist", # Definition lists + "html_image", # HTML images + "tasklist", # Task lists ] -myst_heading_anchors = 5 -myst_fence_as_directive = ["mermaid"] -# Add any paths that contain templates here, relative to this directory. -# templates_path = ["_templates"] # disable it for nvidia-sphinx-theme to show footer +# Templates path +templates_path = ["_templates"] +# List of patterns to ignore when looking for source files +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "build"] # -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = "nvidia_sphinx_theme" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] -# html_js_files = ["custom.js"] -# html_css_files = ["custom.css"] # Not needed with new theme - html_theme_options = { "collapse_navigation": False, "github_url": "https://github.com/ai-dynamo/dynamo", - # "switcher": { - # use for local testing - # "json_url": "http://localhost:8000/_static/switcher.json", - # "json_url": "https://docs.nvidia.com/dynamo/latest/_static/switcher.json", - # "version_match": one_before if "dev" in version_long else version_short, - # }, - "navbar_start": ["navbar-logo", "version-switcher"], + "navbar_start": ["navbar-logo"], "primary_sidebar_end": [], } -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options.update( - { - "collapse_navigation": False, - } -) - -deploy_ngc_org = "nvidia" -deploy_ngc_team = "dynamo" -myst_substitutions = { - "VersionNum": version_short, - "deploy_ngc_org_team": f"{deploy_ngc_org}/{deploy_ngc_team}" - if deploy_ngc_team - else deploy_ngc_org, -} - - -def ultimateReplace(app, docname, source): - result = source[0] - for key in app.config.ultimate_replacements: - result = result.replace(key, app.config.ultimate_replacements[key]) - source[0] = result - - -# this is a necessary hack to allow us to fill in variables that exist in code blocks -ultimate_replacements = { - "{VersionNum}": version_short, - "{SamplesVersionNum}": version_short, - "{NgcOrgTeam}": f"{deploy_ngc_org}/{deploy_ngc_team}" - if deploy_ngc_team - else deploy_ngc_org, -} - -# bibtex_bibfiles = ["references.bib"] -# To test that style looks good with common bibtex config -# bibtex_reference_style = "author_year" -# bibtex_default_style = "plain" - -### We currently use Myst: https://myst-nb.readthedocs.io/en/latest/use/execute.html -nb_execution_mode = "off" # Global execution disable -# execution_excludepatterns = ['tutorials/tts-python-basics.ipynb'] # Individual notebook disable - -############################### -# SETUP SWITCHER -############################### -switcher_path = os.path.join(html_static_path[0], "switcher.json") -versions = [] -# Triton 2 releases -correction = -1 if "dev" in version_long else 0 -upper_bound = version_short.split(".")[1] -for i in range(2, int(version_short.split(".")[1]) + correction): - versions.append((f"2.{i}.0", f"dynamo{i}0")) - -# Patch releases -# Add here. - -versions = sorted(versions, key=lambda v: Version(v[0]), reverse=True) - -# Build switcher data -json_data = [] -for v in versions: - json_data.append( - { - "name": v[0], - "version": v[0], - "url": f"https://docs.nvidia.com/dynamo/archives/{v[1]}/user-guide/docs", - } - ) -if "dev" in version_long: - json_data.insert( - 0, - { - "name": f"{one_before} (current_release)", - "version": f"{one_before}", - "url": "https://docs.nvidia.com/dynamo/latest/index.html", - }, - ) -else: - json_data.insert( - 0, - { - "name": f"{version_short} (current release)", - "version": f"{version_short}", - "url": "https://docs.nvidia.com/dynamo/latest/index.html", - }, - ) - -# Trim to last N releases. -json_data = json_data[0:12] - -json_data.append( - { - "name": "older releases", - "version": "archives", - "url": "https://docs.nvidia.com/dynamo/archives/", - } -) +# Document settings +master_doc = "index" +html_title = f"{project} Documentation" +html_short_title = project +html_baseurl = "https://docs.nvidia.com/dynamo/latest/" -# validate the links -for i, d in enumerate(json_data): - h = httplib2.Http() - resp = h.request(d["url"], "HEAD") - if int(resp[0]["status"]) >= 400: - print(d["url"], "NOK", resp[0]["status"]) - # exit(1) +# Suppress warnings for external links and missing references +suppress_warnings = [ + "myst.xref_missing", # Missing cross-references of relative links outside docs folder +] -# Write switcher data to file -with open(switcher_path, "w") as f: - json.dump(json_data, f, ensure_ascii=False, indent=4) +# Additional MyST configuration +myst_heading_anchors = 7 # Generate anchors for headers +myst_substitutions = {} # Custom substitutions diff --git a/docs/examples/README.md b/docs/examples/README.md deleted file mode 100644 index 560360cd62..0000000000 --- a/docs/examples/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Examples of using Dynamo Platform - -## Serving examples locally - -Follow individual examples under components/backends/ to serve models locally. - -For example follow the [vLLM Backend Example](../../components/backends/vllm/README.md) - -For a basic GPU - unaware example see the [Hello World Example](../../examples/runtime/hello_world/README.md) - -## Deploying Examples to Kubernetes - -First you need to install the Dynamo Cloud Platform. Dynamo Cloud acts as an orchestration layer between the end user and Kubernetes, handling the complexity of deploying your graphs for you. -Before you can deploy your graphs, you need to deploy the Dynamo Runtime and Dynamo Cloud images. This is a one-time action, only necessary the first time you deploy a DynamoGraph. - -### Instructions for Dynamo User -If you are a **👤 Dynamo User** first follow the [Quickstart Guide](../guides/dynamo_deploy/quickstart.md) first. - -### Instructions for Dynamo Contributor -If you are a **🧑‍💻 Dynamo Contributor** you may have to rebuild the dynamo platform images as the code evolves. -For more details read the [Cloud Guide](../guides/dynamo_deploy/dynamo_cloud.md) -Read more on deploying Dynamo Cloud read [deploy/cloud/helm/README.md](../../deploy/cloud/helm/README.md). - - -### Deploying a particular example - -```bash -# Set your dynamo root directory -cd -export PROJECT_ROOT=$(pwd) -export NAMESPACE= # the namespace you used to deploy Dynamo cloud to. -``` - -Deploying an example consists of the simple `kubectl apply -f ... -n ${NAMESPACE}` command. For example: - -```bash -kubectl apply -f components/backends/vllm/deploy/agg.yaml -n ${NAMESPACE} -``` - -You can use `kubectl get dynamoGraphDeployment -n ${NAMESPACE}` to view your deployment. -You can use `kubectl delete dynamoGraphDeployment -n ${NAMESPACE}` to delete the deployment. - -We provide a Custom Resource yaml file for many examples under the `components/backends//deploy/`folder. -Consult the examples below for the CRs for your specific inference backend. - -[View SGLang k8s](../../components/backends/sglang/deploy/README.md) - -[View vLLM K8s](../../components/backends/vllm/deploy/README.md) - -[View TRTLLM k8s](../../components/backends/trtllm/deploy/README.md) - -**Note 1** Example Image - -The examples use a prebuilt image from the `nvcr.io` registry. -You can build your own image and update the image location in your CR file prior to applying. -You could build your own image using - -```bash -./container/build.sh --framework -``` - -For example for the `sglang` run -```bash -./container/build.sh --framework sglang -``` - -Then you would need to overwrite the image in the examples. - -```bash -extraPodSpec: - mainContainer: - image: -``` - -**Note 2** -Setup port forward if needed when deploying to Kubernetes. - -List the services in your namespace: - -```bash -kubectl get svc -n ${NAMESPACE} -``` -Look for one that ends in `-frontend` and use it for port forward. - -```bash -SERVICE_NAME=$(kubectl get svc -n ${NAMESPACE} -o name | grep frontend | sed 's|.*/||' | sed 's|-frontend||' | head -n1) -kubectl port-forward svc/${SERVICE_NAME}-frontend 8080:8080 -n ${NAMESPACE} -``` - -Consult the [Port Forward Documentation](https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/) - - diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 120000 index 0000000000..6fa53604d9 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1 @@ +../../examples/README.md \ No newline at end of file diff --git a/docs/guides/backend.md b/docs/guides/backend.md index 68b0e98432..e0ed764337 100644 --- a/docs/guides/backend.md +++ b/docs/guides/backend.md @@ -76,7 +76,7 @@ The `model_type` can be: See `components/backends` for full code examples. -### Component names +## Component names A worker needs three names to register itself: namespace.component.endpoint diff --git a/docs/guides/dynamo_deploy/dynamo_cloud.md b/docs/guides/dynamo_deploy/dynamo_cloud.md index 0264e6d056..4cc3753024 100644 --- a/docs/guides/dynamo_deploy/dynamo_cloud.md +++ b/docs/guides/dynamo_deploy/dynamo_cloud.md @@ -39,7 +39,7 @@ helm version # v3.0+ docker version # Running daemon # Set your inference runtime image -export DYNAMO_IMAGE=nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.4.0 +export DYNAMO_IMAGE=nvcr.io/nvidia/ai-dynamo/vllm-runtime:0.4.1 # Also available: sglang-runtime, tensorrtllm-runtime ``` @@ -53,7 +53,7 @@ Install from [NGC published artifacts](https://catalog.ngc.nvidia.com/orgs/nvidi ```bash # 1. Set environment export NAMESPACE=dynamo-kubernetes -export RELEASE_VERSION=0.4.0 # any version of Dynamo 0.3.2+ +export RELEASE_VERSION=0.4.1 # any version of Dynamo 0.3.2+ # 2. Install CRDs helm fetch https://helm.ngc.nvidia.com/nvidia/ai-dynamo/charts/dynamo-crds-${RELEASE_VERSION}.tgz @@ -79,7 +79,7 @@ export NAMESPACE=dynamo-cloud export DOCKER_SERVER=nvcr.io/nvidia/ai-dynamo/ # or your registry export DOCKER_USERNAME='$oauthtoken' export DOCKER_PASSWORD= -export IMAGE_TAG=0.4.0 +export IMAGE_TAG=0.4.1 # 2. Build operator cd deploy/cloud/operator @@ -178,4 +178,4 @@ kubectl create secret generic hf-token-secret \ - [GKE-specific setup](gke_setup.md) - [Create custom deployments](create_deployment.md) -- [Dynamo Operator details](dynamo_operator.md) \ No newline at end of file +- [Dynamo Operator details](dynamo_operator.md) diff --git a/docs/guides/dynamo_deploy/dynamo_operator.md b/docs/guides/dynamo_deploy/dynamo_operator.md index 4d3c2a04eb..960719f3a6 100644 --- a/docs/guides/dynamo_deploy/dynamo_operator.md +++ b/docs/guides/dynamo_deploy/dynamo_operator.md @@ -93,7 +93,7 @@ The GitOps workflow for Dynamo deployments consists of three main steps: ### Step 1: Build and Push Dynamo Cloud Operator -First, follow to [See Install Dynamo Cloud](quickstart.md#install-dynamo-cloud). +First, follow to [See Install Dynamo Cloud](README.md). ### Step 2: Create Initial Deployment diff --git a/docs/guides/dynamo_deploy/operator_deployment.md b/docs/guides/dynamo_deploy/operator_deployment.md deleted file mode 120000 index 80ca4341ee..0000000000 --- a/docs/guides/dynamo_deploy/operator_deployment.md +++ /dev/null @@ -1 +0,0 @@ -../../../guides/dynamo_deploy/operator_deployment.md \ No newline at end of file diff --git a/docs/guides/dynamo_deploy/quickstart.md b/docs/guides/dynamo_deploy/quickstart.md deleted file mode 100644 index 28910e98dd..0000000000 --- a/docs/guides/dynamo_deploy/quickstart.md +++ /dev/null @@ -1,196 +0,0 @@ -# Quickstart - -Your onboarding includes 2 steps. -1. Before deploying your inference graphs you need to install the Dynamo Inference Platform and the Dynamo Cloud. -Dynamo Cloud acts as an orchestration layer between the end user and Kubernetes, handling the complexity of deploying your graphs for you. -You could install from [Published Artifacts](#1-installing-dynamo-cloud-from-published-artifacts) or [Source](#2-installing-dynamo-cloud-from-source) -2. Once you install the Dynamo Cloud, proceed to the [Examples](../../examples/README.md) to deploy an inference graph. - -## 1. Installing Dynamo Cloud from Published Artifacts - -Use this approach when installing from pre-built helm charts and docker images published to NGC. - -### Prerequisites - -```bash -export NAMESPACE=dynamo-cloud -export RELEASE_VERSION=0.4.0 -``` - -Install `envsubst`, `kubectl`, `helm` - -### Authenticate with NGC - -Go to https://ngc.nvidia.com/org to get your NGC_CLI_API_KEY. - -```bash -helm repo add nvidia https://helm.ngc.nvidia.com/nvidia --username='$oauthtoken' --password= -``` - -### Fetch Helm Charts - -```bash -# Fetch the CRDs helm chart -helm fetch https://helm.ngc.nvidia.com/nvidia/ai-dynamo/charts/dynamo-crds-${RELEASE_VERSION}.tgz - -# Fetch the platform helm chart -helm fetch https://helm.ngc.nvidia.com/nvidia/ai-dynamo/charts/dynamo-platform-${RELEASE_VERSION}.tgz -``` - -### Install Dynamo Cloud - -**Step 1: Install Custom Resource Definitions (CRDs)** - -```bash -helm install dynamo-crds dynamo-crds-${RELEASE_VERSION}.tgz \ - --namespace default \ - --wait \ - --atomic -``` - -**Step 2: Install Dynamo Platform** - -```bash -kubectl create namespace ${NAMESPACE} - -helm install dynamo-platform dynamo-platform-${RELEASE_VERSION}.tgz --namespace ${NAMESPACE} -``` - -## 2. Installing Dynamo Cloud from Source - -Use this approach when developing or customizing Dynamo as a contributor, or using local helm charts from the source repository. - -### Prerequisites - -Ensure you have the source code checked out and are in the `dynamo` directory: - - -### Set Environment Variables - -Our examples use the [`nvcr.io`](https://catalog.ngc.nvidia.com) but you can setup your own values if you use another docker registry. - -```bash -export NAMESPACE=dynamo-cloud # or whatever you prefer. -export DOCKER_SERVER=nvcr.io/nvidia/ai-dynamo/ # your-docker-registry.com -export DOCKER_USERNAME='$oauthtoken' # your-username if not using nvcr.io -export DOCKER_PASSWORD=YOUR_NGC_CLI_API_KEY # your-password if not using nvcr.io -``` - -### Pick the Dynamo Inference Image - -Export the tag of the Dynamo Runtime Image. -If you are using a pre-defined release: - -```bash -export IMAGE_TAG=RELEASE_VERSION # i.e. 0.3.2 - the release you are using -``` - -Or build your own image first and tag it with IMAGE_TAG - -```bash -export IMAGE_TAG= -./container/build.sh -docker tag dynamo:latest-vllm /dynamo-base:$IMAGE_TAG -docker login -docker push /dynamo-base:latest-vllm -``` - -### Install Dynamo Cloud - -You need to build and push the Dynamo Cloud Operator Image by running - -```bash -cd deploy/cloud/operator -earthly --push +docker --DOCKER_SERVER=$DOCKER_SERVER --IMAGE_TAG=$IMAGE_TAG -``` - -The Nvidia Cloud Operator image will be pulled from the `$DOCKER_SERVER/dynamo-operator:$IMAGE_TAG`. - -You could run the `deploy.sh` or use the manual commands under Step 1 and Step 2. - -**Installing with a script (alternative to the Step 1 and Step 2)** - -Create the namespace and the docker registry secret. - -```bash -kubectl create namespace ${NAMESPACE} -kubectl create secret docker-registry docker-imagepullsecret \ - --docker-server=${DOCKER_SERVER} \ - --docker-username=${DOCKER_USERNAME} \ - --docker-password=${DOCKER_PASSWORD} \ - --namespace=${NAMESPACE} -``` - -You need to add the bitnami helm repository by running: - -```bash -helm repo add bitnami https://charts.bitnami.com/bitnami -``` - -```bash -./deploy.sh --crds -``` - -if you want guidance during the process, run the deployment script with the `--interactive` flag: - -```bash -./deploy.sh --crds --interactive -``` - -**Installing CRDs manually (alternative to the script deploy.sh)** - -***Step 1: Install Custom Resource Definitions (CRDs)** - -```bash -helm install dynamo-crds ./crds/ \ - --namespace default \ - --wait \ - --atomic -``` - -***Step 2: Build Dependencies and Install Platform** - -```bash -cd deploy/cloud/helm -helm dep build ./platform/ - -kubectl create namespace ${NAMESPACE} - -# Create docker registry secret -kubectl create secret docker-registry docker-imagepullsecret \ - --docker-server=${DOCKER_SERVER} \ - --docker-username=${DOCKER_USERNAME} \ - --docker-password=${DOCKER_PASSWORD} \ - --namespace=${NAMESPACE} - -# Install platform -helm install dynamo-platform ./platform/ \ - --namespace ${NAMESPACE} \ - --set "dynamo-operator.controllerManager.manager.image.repository=${DOCKER_SERVER}/dynamo-operator" \ - --set "dynamo-operator.controllerManager.manager.image.tag=${IMAGE_TAG}" \ - --set "dynamo-operator.imagePullSecrets[0].name=docker-imagepullsecret" -``` - -[More on Deploying to Dynamo Cloud](./dynamo_cloud.md) - -## Uninstall CRDs for a clean start - -We provide a script to uninstall CRDs should you need a clean start. - -```bash -./uninstall.sh -``` - -## Explore Examples - -If deploying to Kubernetes, create a Kubernetes secret containing your sensitive values if needed: - -```bash -export HF_TOKEN=your_hf_token -kubectl create secret generic hf-token-secret \ - --from-literal=HF_TOKEN=${HF_TOKEN} \ - -n ${NAMESPACE} -``` - -Follow the [Examples](../../examples/README.md) -For more details on how to create your own deployments follow [Create Deployment Guide](create_deployment.md) diff --git a/docs/hidden_toctree.rst b/docs/hidden_toctree.rst index 9b7e2d1b07..dd6b1f1701 100644 --- a/docs/hidden_toctree.rst +++ b/docs/hidden_toctree.rst @@ -4,18 +4,6 @@ SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0 - Licensed 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. - .. This hidden toctree includes readmes etc that aren't meant to be in the main table of contents but should be accounted for in the sphinx project structure @@ -34,24 +22,32 @@ API/nixl_connect/writable_operation.md API/nixl_connect/read_operation.md API/nixl_connect/write_operation.md - components/backends/sglang/deploy/README.md - components/backends/sglang/docs/dsr1-wideep-h100.md - components/backends/sglang/docs/multinode-examples.md - components/backends/sglang/docs/sgl-http-server.md - components/backends/sglang/slurm_jobs/README.md - examples/README.md + API/nixl_connect/README.md + guides/dynamo_deploy/create_deployment.md guides/dynamo_deploy/sla_planner_deployment.md - guides/dynamo_deploy/helm_install.md guides/dynamo_deploy/gke_setup.md + guides/dynamo_deploy/grove.md + guides/dynamo_deploy/k8s_metrics.md + guides/dynamo_deploy/model_caching_with_fluid.md guides/dynamo_deploy/README.md guides/dynamo_run.md - components/backends/vllm/README.md - components/backends/trtllm/README.md - components/backends/trtllm/deploy/README.md - components/backends/trtllm/llama4_plus_eagle.md - components/backends/trtllm/multinode-examples.md - components/backends/trtllm/kv-cache-transfer.md - components/backends/vllm/deploy/README.md - components/backends/vllm/multi-node.md + guides/metrics.md + guides/run_kvbm_in_vllm.md + + architecture/kv_cache_routing.md + architecture/load_planner.md + architecture/request_migration.md + + components/backends/trtllm/multinode/multinode-examples.md + components/backends/sglang/docs/multinode-examples.md + + examples/README.md + examples/runtime/hello_world/README.md + + architecture/distributed_runtime.md + architecture/dynamo_flow.md + +.. TODO: architecture/distributed_runtime.md and architecture/dynamo_flow.md + have some outdated names/references and need a refresh. diff --git a/docs/index.rst b/docs/index.rst index 822e96b7bb..daac85fdbd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. +.. + Main Page +.. + Welcome to NVIDIA Dynamo ======================== @@ -22,156 +26,49 @@ The NVIDIA Dynamo Platform is a high-performance, low-latency inference framewor .. admonition:: 💎 Discover the latest developments! :class: seealso - This guide is a snapshot of the `Dynamo GitHub Repository `_ at a specific point in time. For the latest information and examples, see: - - - `Dynamo README `_ - - `Architecture and features doc `_ - - `Usage guides `_ - - `Dynamo examples repo `_ - - -Quick Start ------------------ - -Local Deployment -~~~~~~~~~~~~~~~~ - -Get started with Dynamo locally in just a few commands: - -**1. Install Dynamo** - -.. code-block:: bash - - # Install uv (recommended Python package manager) - curl -LsSf https://astral.sh/uv/install.sh | sh - - # Create virtual environment and install Dynamo - uv venv venv - source venv/bin/activate - uv pip install "ai-dynamo[sglang]" # or [vllm], [trtllm] - -**2. Start etcd/NATS** - -.. code-block:: bash - - # Start etcd and NATS using Docker Compose - docker compose -f deploy/docker-compose.yml up -d - -**3. Run Dynamo** - -.. code-block:: bash - - # Start the OpenAI compatible frontend - python -m dynamo.frontend - - # In another terminal, start an SGLang worker - python -m dynamo.sglang.worker deepseek-ai/DeepSeek-R1-Distill-Llama-8B - -**4. Test your deployment** - -.. code-block:: bash - - curl localhost:8080/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{"model": "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", - "messages": [{"role": "user", "content": "Hello!"}], - "max_tokens": 50}' - -Kubernetes Deployment -~~~~~~~~~~~~~~~~~~~~~ - -For deployments on Kubernetes, follow the :doc:`Dynamo Platform Quickstart Guide `. - + This guide is a snapshot at a specific point in time. For the latest information and examples, see the `Dynamo GitHub repository `_. -Dive in: Examples ------------------ - -The examples below assume you build the latest image yourself from source. If using a prebuilt image follow the examples from the corresponding branch. - -.. grid:: 1 2 2 2 - :gutter: 3 - :margin: 0 - :padding: 3 4 0 0 - - .. grid-item-card:: :doc:`Hello World ` - :link: examples/runtime/hello_world/README - :link-type: doc - - Demonstrates the basic concepts of Dynamo by creating a simple GPU-unaware graph - - .. grid-item-card:: :doc:`LLM Serving with VLLM ` - :link: components/backends/vllm/README - :link-type: doc - - Presents examples and reference implementations for deploying Large Language Models (LLMs) in various configurations with VLLM. - - .. grid-item-card:: :doc:`Multinode with SGLang ` - :link: components/backends/sglang/docs/multinode-examples - :link-type: doc - - Demonstrates disaggregated serving on several nodes. - - .. grid-item-card:: :doc:`TensorRT-LLM ` - :link: components/backends/trtllm/README - :link-type: doc - - Presents TensorRT-LLM examples and reference implementations for deploying Large Language Models (LLMs) in various configurations. +Quickstart +========== +.. include:: _includes/quick_start_local.rst +.. + Sidebar +.. .. toctree:: :hidden: + :caption: Getting Started - Welcome to Dynamo + Quickstart + Installation <_sections/installation> Support Matrix + Architecture <_sections/architecture> + Examples <_sections/examples> .. toctree:: :hidden: - :caption: Architecture & Features + :caption: Kubernetes Deployment - High Level Architecture - Distributed Runtime - Disaggregated Serving - KV Block Manager - KV Cache Routing - Planner - Dynamo Architecture Flow + Quickstart (K8s) <../guides/dynamo_deploy/dynamo_cloud.md> + Dynamo Operator <../guides/dynamo_deploy/dynamo_operator.md> + Metrics <../guides/dynamo_deploy/k8s_metrics.md> + Multinode <../guides/dynamo_deploy/multinode-deployment.md> + Minikube Setup <../guides/dynamo_deploy/minikube.md> .. toctree:: :hidden: - :caption: Using Dynamo + :caption: Components - Writing Python Workers in Dynamo - Disaggregation and Performance Tuning - Working with Dynamo Kubernetes Operator + Backends <_sections/backends> + Router + Planner + KVBM .. toctree:: :hidden: - :caption: Deployment Guides - - Dynamo Deploy Quickstart - Dynamo Cloud Kubernetes Platform - Manual Helm Deployment - Minikube Setup Guide - Model Caching with Fluid - -.. toctree:: - :hidden: - :caption: Examples - - Hello World - LLM Deployment Examples using VLLM - LLM Deployment Examples using SGLang - Multinode Examples using SGLang - Planner Benchmark Example - LLM Deployment Examples using TensorRT-LLM - -.. toctree:: - :hidden: - :caption: Reference - + :caption: Developer Guide + Tuning Disaggregated Serving Performance + Writing Python Workers in Dynamo Glossary - NIXL Connect API - KVBM Reading - - diff --git a/examples/basics/multinode/README.md b/examples/basics/multinode/README.md index de5c2c17ca..ac2db06adb 100644 --- a/examples/basics/multinode/README.md +++ b/examples/basics/multinode/README.md @@ -315,6 +315,7 @@ Send multiple new conversations to see them distributed across replicas: ```python import asyncio from openai import AsyncOpenAI +import os if os.environ.get("DYN_FRONTEND_IP"): frontend_ip=os.environ.get("DYN_FRONTEND_IP") diff --git a/examples/runtime/hello_world/README.md b/examples/runtime/hello_world/README.md index 2063aaa36c..97a363a868 100644 --- a/examples/runtime/hello_world/README.md +++ b/examples/runtime/hello_world/README.md @@ -106,7 +106,7 @@ Hello star! Note that this a very simple degenerate example which does not demonstrate the standard Dynamo FrontEnd-Backend deployment. The hello-world client is not a web server, it is a one-off function which sends the predefined text "world,sun,moon,star" to the backend. The example is meant to show the HelloWorldWorker. As such you will only see the HelloWorldWorker pod in deployment. The client will run and exit and the pod will not be operational. -Follow the [Quickstart Guide](../../../docs/guides/dynamo_deploy/quickstart.md) to install Dynamo Kubernetes Platform. +Follow the [Quickstart Guide](../../../docs/guides/dynamo_deploy/README.md) to install Dynamo Kubernetes Platform. Then deploy to kubernetes using ```bash @@ -119,4 +119,4 @@ to delete your deployment: ```bash kubectl delete dynamographdeployment hello-world -n ${NAMESPACE} -``` \ No newline at end of file +``` From 1948113c770d049a10a134e346e229f58b4ed473 Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Tue, 26 Aug 2025 11:07:45 -0700 Subject: [PATCH 31/82] feat: parse normal text along with tool calls (#2709) Signed-off-by: Jason Zhou --- .../openai/chat_completions/aggregator.rs | 72 ++- lib/parsers/src/tool_calling/json_parser.rs | 115 +++-- lib/parsers/src/tool_calling/parsers.rs | 422 +++++++++++++++--- lib/parsers/src/tool_calling/tools.rs | 80 ++-- 4 files changed, 551 insertions(+), 138 deletions(-) diff --git a/lib/llm/src/protocols/openai/chat_completions/aggregator.rs b/lib/llm/src/protocols/openai/chat_completions/aggregator.rs index ca0bd3849c..a8e5b5f4e2 100644 --- a/lib/llm/src/protocols/openai/chat_completions/aggregator.rs +++ b/lib/llm/src/protocols/openai/chat_completions/aggregator.rs @@ -166,7 +166,7 @@ impl DeltaAggregator { // After aggregation, inspect each choice's text for tool call syntax for choice in aggregator.choices.values_mut() { if choice.tool_calls.is_none() - && let Ok(tool_calls) = try_tool_call_parse_aggregate( + && let Ok((tool_calls, normal_text)) = try_tool_call_parse_aggregate( &choice.text, parsing_options.tool_call_parser.as_deref(), ) @@ -184,6 +184,10 @@ impl DeltaAggregator { } choice.tool_calls = Some(tool_calls); choice.text.clear(); + // If normal text is not empty, update the choice text + if let Some(normal_text) = normal_text.filter(|text| !text.is_empty()) { + choice.text = normal_text; + } choice.finish_reason = Some(dynamo_async_openai::types::FinishReason::ToolCalls); } } @@ -223,7 +227,7 @@ impl From for dynamo_async_openai::types::ChatChoice { dynamo_async_openai::types::ChatChoice { message: dynamo_async_openai::types::ChatCompletionResponseMessage { role: delta.role.expect("delta should have a Role"), - content: if delta.tool_calls.is_some() { + content: if delta.text.is_empty() { None } else { Some(delta.text) @@ -582,4 +586,68 @@ mod tests { dynamo_async_openai::types::Role::Assistant ); } + + #[tokio::test] + async fn test_tool_calling_output_with_normal_text() { + // Simulate a delta with a tool call in the content + let tool_call_json = r#"Hey, I'm a normal text! {"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}"#; + + // Use create_test_delta to generate the annotated delta, then extract the inner delta for the test + let annotated_delta = create_test_delta( + 0, + tool_call_json, + Some(dynamo_async_openai::types::Role::Assistant), + Some(dynamo_async_openai::types::FinishReason::ToolCalls), + ); + let data = annotated_delta.data.unwrap(); + + // Wrap it in Annotated and create a stream + let annotated_delta = Annotated { + data: Some(data), + id: Some("test_id".to_string()), + event: None, + comment: None, + }; + let stream = Box::pin(stream::iter(vec![annotated_delta])); + + // Call DeltaAggregator::apply + let result = DeltaAggregator::apply(stream, ParsingOptions::default()).await; + + // Check the result + assert!(result.is_ok()); + let response = result.unwrap(); + + // There should be one choice + assert_eq!(response.choices.len(), 1); + let choice = &response.choices[0]; + + // The tool_calls field should be present and parsed + assert!(choice.message.tool_calls.is_some()); + let tool_calls = choice.message.tool_calls.as_ref().unwrap(); + assert_eq!(tool_calls.len(), 1); + + let tool_call = &tool_calls[0]; + assert_eq!(tool_call.function.name, "get_weather"); + // The arguments should be a JSON string containing the expected keys + let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments).unwrap(); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + + // The content should be the normal text + assert!(choice.message.content.is_some()); + assert_eq!( + choice.message.content.as_ref().unwrap(), + "Hey, I'm a normal text!" + ); + + // The finish_reason should be ToolCalls + assert_eq!( + choice.finish_reason, + Some(dynamo_async_openai::types::FinishReason::ToolCalls) + ); + assert_eq!( + choice.message.role, + dynamo_async_openai::types::Role::Assistant + ); + } } diff --git a/lib/parsers/src/tool_calling/json_parser.rs b/lib/parsers/src/tool_calling/json_parser.rs index 0357a27a16..72fe7fa1d5 100644 --- a/lib/parsers/src/tool_calling/json_parser.rs +++ b/lib/parsers/src/tool_calling/json_parser.rs @@ -60,10 +60,10 @@ fn extract_tool_call_content(input: &str, start_token: &str, end_token: &str) -> // Special case for <|python_tag|> . Regex pattern does not work well with it as it has no end token // Handles single tool and multiple tool call cases for single start_token like <|python_tag|> -fn handle_single_token_tool_calls(input: &str, start_token: &str) -> String { +fn handle_single_token_tool_calls(input: &str, start_token: &str) -> Option { // Return the input if it doesn't contain the start token if !input.contains(start_token) { - return input.to_string(); + return None; } // Split on the start token and keep only JSON-looking segments @@ -89,13 +89,23 @@ fn handle_single_token_tool_calls(input: &str, start_token: &str) -> String { // Remove everything up to and including the first occurrence of the start token if let Some(idx) = input.find(start_token) { let rest = &input[idx + start_token.len()..]; - return rest.trim_start().to_string(); + return Some(rest.trim_start().to_string()); } else { // Shouldn't happen because we checked contains() above, but be defensive - return input.to_string(); + return None; } } - format!("[{}]", items.join(",")) + Some(format!("[{}]", items.join(","))) +} + +fn try_parse_normal_text(input: &str, start_token: &str) -> String { + // If input contains start token, just take the part before it + if let Some(idx) = input.find(start_token) { + return input[..idx].trim().to_string(); + } + + // No start token found, return empty string + String::new() } /// Attempts to parse a tool call from a raw LLM message string into a unified [`ToolCallResponse`] format. @@ -142,40 +152,81 @@ fn handle_single_token_tool_calls(input: &str, start_token: &str) -> String { pub fn try_tool_call_parse_json( message: &str, config: &JsonParserConfig, -) -> anyhow::Result> { +) -> anyhow::Result<(Vec, Option)> { // Log the config we are using tracing::debug!("Using JSON parser config: {:?}", config); let trimmed = message.trim(); - // Use config to get tool call start and end token vectors, then use the first element for now + // Early exit if no content + if trimmed.is_empty() { + return Ok((vec![], Some(String::new()))); + } + let tool_call_start_tokens = &config.tool_call_start_tokens; let tool_call_end_tokens = &config.tool_call_end_tokens; - assert!( - tool_call_start_tokens.len() == tool_call_end_tokens.len(), - "Tool call start and end tokens must have the same length" - ); + // Early exit if no tokens configured + if tool_call_start_tokens.is_empty() { + return Ok((vec![], Some(trimmed.to_string()))); + } // Iterate over all start and end tokens and try to extract the content between them // Assumption : One message will not contain different tags for tool calls. Iteration over tags is to support different tags by default for multiple models let mut json = trimmed.to_string(); - for (start_token, end_token) in tool_call_start_tokens + let mut normal_text = trimmed.to_string(); + + // First, check if ANY start token exists in the input + let has_start_token = tool_call_start_tokens .iter() - .zip(tool_call_end_tokens.iter()) - { - // Special case for <|python_tag|> . Regex pattern does not work well with it as it has no end token - json = if !start_token.is_empty() && end_token.is_empty() { - handle_single_token_tool_calls(&json, start_token) - } else if let Some(content) = extract_tool_call_content(&json, start_token, end_token) { - content - } else { - json - }; - } + .any(|token| !token.is_empty() && normal_text.contains(token)); + if !has_start_token { + // No start tokens found, try to extract JSON directly. Everything that starts with { or [ is considered a potential JSON. + if let Some(idx) = normal_text.find(['{', '[']) { + let extracted_normal = normal_text[..idx].trim().to_string(); + let extracted_json = normal_text[idx..].trim().to_string(); + if !extracted_json.is_empty() { + normal_text = extracted_normal; + json = extracted_json; + } + } + } else { + // Start tokens exist, use regex-based parsing + for (start_token, end_token) in tool_call_start_tokens + .iter() + .zip(tool_call_end_tokens.iter()) + { + let new_normal_text = try_parse_normal_text(&normal_text, start_token); + + // Process based on token types + match (start_token.is_empty(), end_token.is_empty()) { + (false, true) => { + // Single token case + let result = handle_single_token_tool_calls(&json, start_token); + if let Some(content) = result { + json = content; + // For single token case, use the normal text we extracted earlier + normal_text = new_normal_text; + break; // Found content, exit early + } + } + (false, false) => { + // Start and end token case + let result = extract_tool_call_content(&json, start_token, end_token); + if let Some(content) = result { + json = content; + normal_text = new_normal_text; + break; // Found content, exit early + } + } + _ => { + continue; + } + } + } + } // Convert json (String) to &str let json = json.as_str(); - // Anonymous function to attempt deserialization into a known representation let parse = |name: String, args: HashMap| -> anyhow::Result<_> { Ok(ToolCallResponse { @@ -198,7 +249,10 @@ pub fn try_tool_call_parse_json( // } // } if let Ok(single) = serde_json::from_str::(json) { - return Ok(vec![parse(single.name, single.parameters)?]); + return Ok(( + vec![parse(single.name, single.parameters)?], + Some(normal_text), + )); //parse(single.name, single.parameters).map(Some); // CalledFunctionArguments: Single { name, arguments } @@ -211,7 +265,10 @@ pub fn try_tool_call_parse_json( // } // } } else if let Ok(single) = serde_json::from_str::(json) { - return Ok(vec![parse(single.name, single.arguments)?]); + return Ok(( + vec![parse(single.name, single.arguments)?], + Some(normal_text), + )); // Vec: List of { name, parameters } // Example: @@ -225,7 +282,7 @@ pub fn try_tool_call_parse_json( for item in list { results.push(parse(item.name, item.parameters)?); } - return Ok(results); + return Ok((results, Some(normal_text))); // Vec: List of { name, arguments } // Example: @@ -244,8 +301,8 @@ pub fn try_tool_call_parse_json( for item in list { results.push(parse(item.name, item.arguments)?); } - return Ok(results); + return Ok((results, Some(normal_text))); } - Ok(vec![]) + Ok((vec![], Some(trimmed.to_string()))) } diff --git a/lib/parsers/src/tool_calling/parsers.rs b/lib/parsers/src/tool_calling/parsers.rs index c703f7e03d..6e9d420798 100644 --- a/lib/parsers/src/tool_calling/parsers.rs +++ b/lib/parsers/src/tool_calling/parsers.rs @@ -135,10 +135,13 @@ pub struct ToolCallConfig { pub fn try_tool_call_parse( message: &str, config: &ToolCallConfig, -) -> anyhow::Result> { +) -> anyhow::Result<(Vec, Option)> { // Use match statement (Rust's switch statement) to call the appropriate parser match config.format { - ToolCallParserType::Json => try_tool_call_parse_json(message, &config.json), + ToolCallParserType::Json => { + let (results, normal_content) = try_tool_call_parse_json(message, &config.json)?; + Ok((results, normal_content)) + } ToolCallParserType::Harmony => { anyhow::bail!("Harmony parser not implemented"); } @@ -158,7 +161,7 @@ pub fn try_tool_call_parse( pub fn detect_and_parse_tool_call( message: &str, parser_str: Option<&str>, -) -> anyhow::Result> { +) -> anyhow::Result<(Vec, Option)> { let mut parser_map: std::collections::HashMap<&str, ToolCallConfig> = std::collections::HashMap::new(); parser_map.insert("hermes", ToolCallConfig::hermes()); @@ -175,7 +178,10 @@ pub fn detect_and_parse_tool_call( }; match parser_map.get(parser_key) { - Some(config) => try_tool_call_parse(message, config), + Some(config) => { + let (results, normal_content) = try_tool_call_parse(message, config)?; + Ok((results, normal_content)) + } None => anyhow::bail!("Parser for the given config is not implemented"), // Original message } } @@ -194,7 +200,8 @@ mod tests { #[test] fn parses_single_parameters_object() { let input = r#"{ "name": "hello", "parameters": { "x": 1, "y": 2 } }"#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -206,7 +213,8 @@ mod tests { #[test] fn parses_single_arguments_object() { let input = r#"{ "name": "world", "arguments": { "a": "abc", "b": 42 } }"#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -218,7 +226,8 @@ mod tests { #[test] fn parses_vec_of_parameters() { let input = r#"[{ "name": "first", "parameters": { "a": 1 } }, { "name": "second", "parameters": { "b": 2 } }]"#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -232,7 +241,8 @@ mod tests { #[test] fn parses_vec_of_arguments() { let input = r#"[{ "name": "alpha", "arguments": { "a": "x" } }, { "name": "omega", "arguments": { "z": "y" } }]"#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -247,7 +257,8 @@ mod tests { fn parses_toolcall_wrapped_payload() { let input = r#"[{ "name": "wrapped", "parameters": { "foo": "bar" } }]"#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -258,7 +269,7 @@ mod tests { #[test] fn parses_python_tag_prefixed_payload() { let input = r#"<|python_tag|>{ "name": "pyfunc", "arguments": { "k": "v" } }"#; - let result = try_tool_call_parse( + let (result, content) = try_tool_call_parse( input, &ToolCallConfig { format: ToolCallParserType::Json, @@ -270,6 +281,7 @@ mod tests { }, ) .unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -280,14 +292,16 @@ mod tests { #[test] fn returns_none_on_invalid_input() { let input = r#"not even json"#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some("not even json".to_string())); assert!(result.is_empty()); } #[test] fn returns_none_on_valid_json_wrong_shape() { let input = r#"{ "foo": "bar" }"#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some("{ \"foo\": \"bar\" }".to_string())); assert!(result.is_empty()); } @@ -299,9 +313,23 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}]"#; - let result = detect_and_parse_tool_call(input, Some("nemotron_deci")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("nemotron_deci")).unwrap(); assert!(!result.is_empty()); assert_eq!(result.len(), 1); + assert_eq!(content, Some("\nOkay, the user is asking for the weather in San Francisco in Fahrenheit. Let me check the tools available.\n".to_string())); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_nvidia_llama3_nemotron_super_49b_simple_with_no_think() { + let input = r#"[{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}]"#; + let (result, content) = detect_and_parse_tool_call(input, Some("nemotron_deci")).unwrap(); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + assert_eq!(content, Some("".to_string())); let (name, args) = extract_name_and_args(result[0].clone()); assert_eq!(name, "get_weather"); assert_eq!(args["location"], "San Francisco, CA"); @@ -316,7 +344,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}}]"#; let config = ToolCallConfig::nemotron_deci(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("\nOkay, the user is asking for the weather in San Francisco in Fahrenheit. Let me check the tools available.\n".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -346,7 +375,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me "#; let config = ToolCallConfig::nemotron_deci(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("\nOkay, the user is asking for the weather in San Francisco in Fahrenheit. Let me check the tools available.\n".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -364,7 +394,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me let input = r#" {"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}} "#; - let result = detect_and_parse_tool_call(input, Some("hermes")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("hermes")).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -373,12 +404,24 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me assert_eq!(args["unit"], "fahrenheit"); } + #[test] + fn test_qwen_qwq_32b_simple_with_normal_text() { + let input = r#"Hey How are you? +{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}} +"#; + let (result, content) = detect_and_parse_tool_call(input, Some("hermes")).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + } + #[test] fn test_nousresearch_hermes3_llama31_8b_simple() { let input = r#" {"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}} "#; - let result = detect_and_parse_tool_call(input, Some("hermes")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("hermes")).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -397,7 +440,32 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me "#; let config = ToolCallConfig::hermes(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 2); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + let (name, args) = extract_name_and_args(result[1].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "New York, NY"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_qwen_qwq_32b_multiple_tool_calls_with_normal_text() { + let input = r#"Hey How are you? +{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}} + + +{"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}} + +"#; + let config = ToolCallConfig::hermes(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -424,7 +492,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me "#; let config = ToolCallConfig::hermes(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -450,7 +519,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me ..Default::default() }, }; - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -462,16 +532,23 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me #[test] fn test_mistralai_mistral_7b_instruct_v03_simple() { let input = r#" [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}]"#; - let config = ToolCallConfig { - format: ToolCallParserType::Json, - json: JsonParserConfig { - tool_call_start_tokens: vec![], - tool_call_end_tokens: vec![], - arguments_keys: vec!["arguments".to_string()], - ..Default::default() - }, - }; - let result = try_tool_call_parse(input, &config).unwrap(); + let config = ToolCallConfig::mistral(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_mistralai_mistral_7b_instruct_v03_simple_with_normal_text() { + let input = r#"Hey How are you? [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}]"#; + let config = ToolCallConfig::mistral(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -488,16 +565,9 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me "San Francisco, CA", "unit": "fahrenheit"}}] "#; - let config = ToolCallConfig { - format: ToolCallParserType::Json, - json: JsonParserConfig { - tool_call_start_tokens: vec![], - tool_call_end_tokens: vec![], - arguments_keys: vec!["arguments".to_string()], - ..Default::default() - }, - }; - let result = try_tool_call_parse(input, &config).unwrap(); + let config = ToolCallConfig::mistral(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -510,7 +580,26 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me fn test_mistralai_mistral_7b_instruct_v03_multiple() { let input = r#" [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}}]"#; let config = ToolCallConfig::mistral(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 2); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + let (name, args) = extract_name_and_args(result[1].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "New York, NY"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_mistralai_mistral_7b_instruct_v03_multiple_with_normal_text() { + let input = r#"Hey How are you? [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}}]"#; + let config = ToolCallConfig::mistral(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -527,12 +616,16 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me fn test_mistralai_mistral_7b_instruct_v03_multiple_with_new_lines() { let input = r#" [{"name": "get_weather", - "arguments": {"location": "San Francisco, CA", - "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": - {"location": "New York, NY", "unit": "fahrenheit"}}] + "arguments": {"location": + "San Francisco, CA", + "unit": "fahrenheit"}}, + {"name": "get_weather", "arguments": + {"location": "New York, NY", "unit": + "fahrenheit"}}] "#; let config = ToolCallConfig::mistral(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -549,7 +642,22 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me fn test_mistralai_mistral_7b_instruct_v03_single_with_start_token() { let input = r#"[TOOL_CALLS] [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}]"#; let config = ToolCallConfig::mistral(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_mistralai_mistral_7b_instruct_v03_single_with_start_token_with_normal_text() { + let input = r#"Hey How are you? [TOOL_CALLS] [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}]"#; + let config = ToolCallConfig::mistral(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -568,7 +676,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me "unit": "fahrenheit"}}] "#; let config = ToolCallConfig::mistral(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -581,7 +690,26 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me fn test_mistralai_mistral_7b_instruct_v03_single_with_start_token_multiple() { let input = r#"[TOOL_CALLS] [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}}]"#; let config = ToolCallConfig::mistral(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 2); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + let (name, args) = extract_name_and_args(result[1].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "New York, NY"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_mistralai_mistral_7b_instruct_v03_single_with_start_token_multiple_with_normal_text() { + let input = r#"Hey How are you? [TOOL_CALLS] [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}}]"#; + let config = ToolCallConfig::mistral(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -607,7 +735,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me "fahrenheit"}}] "#; let config = ToolCallConfig::mistral(); - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -623,7 +752,21 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me #[test] fn test_meta_llama_llama31_8b_instruct_simple() { let input = r#"{"name": "get_weather", "parameters": {"location": "San Francisco, CA", "unit": "fahrenheit"}}"#; - let result = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::mistral()).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_meta_llama_llama31_8b_instruct_simple_with_normal_text() { + let input = r#"Hey How are you? {"name": "get_weather", "parameters": {"location": "San Francisco, CA", "unit": "fahrenheit"}}"#; + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::mistral()).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -638,7 +781,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me {"name": "get_weather", "parameters": {"location": "San Francisco, CA", "unit": "fahrenheit"}} "#; - let result = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -650,7 +794,21 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me #[test] fn test_meta_llama_llama31_8b_instruct_with_python_tag() { let input = r#"<|python_tag|>{ "name": "get_weather", "parameters": {"location": "San Francisco, CA", "unit": "fahrenheit" } }"#; - let result = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_meta_llama_llama31_8b_instruct_with_python_tag_with_normal_text() { + let input = r#"Hey How are you? <|python_tag|>{ "name": "get_weather", "parameters": {"location": "San Francisco, CA", "unit": "fahrenheit" } }"#; + let (result, content) = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -665,7 +823,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me <|python_tag|> {"name": "get_weather", "parameters": {"location": "San Francisco, CA", "unit": "fahrenheit"}} "#; - let result = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -682,7 +841,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me <|python_tag|> {"name": "get_weather", "parameters": {"location": "New York, NY", "unit": "fahrenheit" }} "#; - let result = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("llama3_json")).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -710,15 +870,15 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me // Known parser, but invalid input (not JSON) should return Ok(None) let input = "not a json"; - let result = detect_and_parse_tool_call(input, Some("hermes")); - assert!(result.is_ok()); - assert!(result.unwrap().is_empty()); + let (result, content) = detect_and_parse_tool_call(input, Some("hermes")).unwrap(); + assert_eq!(content, Some("not a json".to_string())); + assert!(result.is_empty()); // Known parser, but valid JSON with wrong shape should return Ok(None) let input = r#"{"foo": "bar"}"#; - let result = detect_and_parse_tool_call(input, Some("hermes")); - assert!(result.is_ok()); - assert!(result.unwrap().is_empty()); + let (result, content) = detect_and_parse_tool_call(input, Some("hermes")).unwrap(); + assert_eq!(content, Some(r#"{"foo": "bar"}"#.to_string())); + assert!(result.is_empty()); } #[test] @@ -729,7 +889,8 @@ Okay, the user is asking for the weather in San Francisco in Fahrenheit. Let me - **Summer (June to August)**: Average highs range from the mid-60s to low 70s Fahrenheit, with cooler mornings and evenings. Coastal areas may be cooler than inland spots. Remember, San Francisco weather can be quite unpredictable, particularly with its famous fog, which can significantly lower temperatures. Always check a local weather forecast for the most accurate and up-to-date information."#; - let result = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::default()).unwrap(); + assert_eq!(content, Some(input.to_string())); assert!(result.is_empty()); // This model doesn't produce tool calls } @@ -748,7 +909,8 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it ..Default::default() }, }; - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -770,7 +932,8 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it ..Default::default() }, }; - let result = try_tool_call_parse(input, &config).unwrap(); + let (result, content) = try_tool_call_parse(input, &config).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -782,7 +945,8 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it #[test] fn test_detect_and_parse_tool_call_default_parser_nemotron_deci() { let input = r#"[{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}]"#; - let result = detect_and_parse_tool_call(input, None).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, None).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -794,7 +958,25 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it #[test] fn test_detect_and_parse_tool_call_default_parser_nemotron_deci_multiple() { let input = r#"[{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}}]"#; - let result = detect_and_parse_tool_call(input, None).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, None).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 2); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + let (name, args) = extract_name_and_args(result[1].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "New York, NY"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_detect_and_parse_tool_call_default_parser_nemotron_deci_multiple_with_normal_text() { + let input = r#"Hey How are you? [{"name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}}, {"name": "get_weather", "arguments": {"location": "New York, NY", "unit": "fahrenheit"}}]"#; + let (result, content) = detect_and_parse_tool_call(input, None).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 2); let (name, args) = extract_name_and_args(result[0].clone()); @@ -810,7 +992,22 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it #[test] fn test_detect_and_parse_tool_call_default_parser_llama3_json_with_python_tag() { let input = r#"<|python_tag|>{ "name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit" } }"#; - let result = detect_and_parse_tool_call(input, None).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, None).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_detect_and_parse_tool_call_default_parser_llama3_json_with_python_tag_with_normal_text() + { + let input = r#"Hey How are you? <|python_tag|>{ "name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit" } }"#; + let (result, content) = detect_and_parse_tool_call(input, None).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -829,7 +1026,8 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it {"location": "San Francisco, CA", "unit": "fahrenheit" }} "#; - let result = detect_and_parse_tool_call(input, None).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, None).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -846,7 +1044,8 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it {"location": "San Francisco, CA", "unit": "fahrenheit" }} "#; - let result = detect_and_parse_tool_call(input, None).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, None).unwrap(); + assert_eq!(content, Some("".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -858,7 +1057,22 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it #[test] fn test_detect_and_parse_tool_call_default_parser_llama3_json_without_python_tag() { let input = r#"{ "name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit" } }"#; - let result = detect_and_parse_tool_call(input, None).unwrap(); + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::mistral()).unwrap(); + assert_eq!(content, Some("".to_string())); + assert!(!result.is_empty()); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather"); + assert_eq!(args["location"], "San Francisco, CA"); + assert_eq!(args["unit"], "fahrenheit"); + } + + #[test] + fn test_detect_and_parse_tool_call_default_parser_llama3_json_without_python_tag_with_normal_text() + { + let input = r#"Hey How are you? { "name": "get_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit" } }"#; + let (result, content) = try_tool_call_parse(input, &ToolCallConfig::mistral()).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert!(!result.is_empty()); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); @@ -871,7 +1085,19 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it fn test_phi4_single_function_call() { let input = r#"functools[{"name": "get_country_capital", "arguments": {"country": "Poland"}}]"#; - let result = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("".to_string())); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_country_capital"); + assert_eq!(args["country"], "Poland"); + } + + #[test] + fn test_phi4_single_function_call_with_normal_text() { + let input = r#"Hey How are you? functools[{"name": "get_country_capital", "arguments": {"country": "Poland"}}]"#; + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); assert_eq!(name, "get_country_capital"); @@ -884,7 +1110,27 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it {"name": "get_country_capital", "arguments": {"country": "Poland"}}, {"name": "get_population", "arguments": {"city": "Warsaw"}} ]"#; - let result = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("".to_string())); + assert_eq!(result.len(), 2); + + let (name1, args1) = extract_name_and_args(result[0].clone()); + assert_eq!(name1, "get_country_capital"); + assert_eq!(args1["country"], "Poland"); + + let (name2, args2) = extract_name_and_args(result[1].clone()); + assert_eq!(name2, "get_population"); + assert_eq!(args2["city"], "Warsaw"); + } + + #[test] + fn test_phi4_multiple_function_calls_simple_arguments_with_normal_text() { + let input = r#"Hey How are you? functools[ + {"name": "get_country_capital", "arguments": {"country": "Poland"}}, + {"name": "get_population", "arguments": {"city": "Warsaw"}} +]"#; + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert_eq!(result.len(), 2); let (name1, args1) = extract_name_and_args(result[0].clone()); @@ -901,7 +1147,23 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it let input = r#"functools[{"name": "get_weather_forecast", "arguments": {"location": {"city": "San Francisco", "state": "CA"}, "date": "2023-10-05"}}]"#; - let result = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("".to_string())); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "get_weather_forecast"); + assert_eq!(args["date"], "2023-10-05"); + assert_eq!(args["location"]["city"], "San Francisco"); + assert_eq!(args["location"]["state"], "CA"); + } + + #[test] + fn test_phi4_single_function_call_nested_json_arguments_with_normal_text() { + let input = r#"Hey How are you? functools[{"name": "get_weather_forecast", "arguments": + {"location": {"city": "San Francisco", + "state": "CA"}, "date": "2023-10-05"}}]"#; + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); assert_eq!(name, "get_weather_forecast"); @@ -914,7 +1176,21 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it fn test_phi4_function_call_with_parameters_instead_of_arguments() { let input = r#"functools[{"name": "calculate_distance", "parameters": {"from": "New York", "to": "Los Angeles"}}]"#; - let result = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("".to_string())); + assert_eq!(result.len(), 1); + let (name, args) = extract_name_and_args(result[0].clone()); + assert_eq!(name, "calculate_distance"); + assert_eq!(args["from"], "New York"); + assert_eq!(args["to"], "Los Angeles"); + } + + #[test] + fn test_phi4_function_call_with_parameters_instead_of_arguments_with_normal_text() { + let input = r#"Hey How are you? functools[{"name": "calculate_distance", + "parameters": {"from": "New York", "to": "Los Angeles"}}]"#; + let (result, content) = detect_and_parse_tool_call(input, Some("phi4")).unwrap(); + assert_eq!(content, Some("Hey How are you?".to_string())); assert_eq!(result.len(), 1); let (name, args) = extract_name_and_args(result[0].clone()); assert_eq!(name, "calculate_distance"); diff --git a/lib/parsers/src/tool_calling/tools.rs b/lib/parsers/src/tool_calling/tools.rs index 284f8d751b..a0defc4a54 100644 --- a/lib/parsers/src/tool_calling/tools.rs +++ b/lib/parsers/src/tool_calling/tools.rs @@ -13,29 +13,35 @@ pub use super::parsers::{ToolCallConfig, detect_and_parse_tool_call}; pub fn try_tool_call_parse_aggregate( message: &str, parser_str: Option<&str>, -) -> anyhow::Result> { +) -> anyhow::Result<( + Vec, + Option, +)> { if parser_str.is_none() { tracing::info!("No tool parser provided. Trying parsing with default parser."); } else { tracing::info!("Using tool parser: {:?}", parser_str); } - let parsed = detect_and_parse_tool_call(message, parser_str)?; + let (parsed, content) = detect_and_parse_tool_call(message, parser_str)?; if parsed.is_empty() { - return Ok(vec![]); + return Ok((vec![], content)); } - Ok(parsed - .into_iter() - .map( - |parsed| dynamo_async_openai::types::ChatCompletionMessageToolCall { - id: parsed.id, - r#type: dynamo_async_openai::types::ChatCompletionToolType::Function, - function: dynamo_async_openai::types::FunctionCall { - name: parsed.function.name, - arguments: parsed.function.arguments, + Ok(( + parsed + .into_iter() + .map( + |parsed| dynamo_async_openai::types::ChatCompletionMessageToolCall { + id: parsed.id, + r#type: dynamo_async_openai::types::ChatCompletionToolType::Function, + function: dynamo_async_openai::types::FunctionCall { + name: parsed.function.name, + arguments: parsed.function.arguments, + }, }, - }, - ) - .collect()) + ) + .collect(), + content, + )) } /// Try parsing a string as a structured tool call, for streaming (delta) usage. @@ -44,25 +50,31 @@ pub fn try_tool_call_parse_aggregate( pub fn try_tool_call_parse_stream( message: &str, parser_str: Option<&str>, -) -> anyhow::Result> { - let parsed = detect_and_parse_tool_call(message, parser_str)?; +) -> anyhow::Result<( + Vec, + Option, +)> { + let (parsed, content) = detect_and_parse_tool_call(message, parser_str)?; if parsed.is_empty() { - return Ok(vec![]); + return Ok((vec![], content)); } - Ok(parsed - .into_iter() - .enumerate() - .map( - |(idx, parsed)| dynamo_async_openai::types::ChatCompletionMessageToolCallChunk { - index: idx as u32, - id: Some(parsed.id), - r#type: Some(dynamo_async_openai::types::ChatCompletionToolType::Function), - function: Some(dynamo_async_openai::types::FunctionCallStream { - name: Some(parsed.function.name), - arguments: Some(parsed.function.arguments), - }), - // Add other fields as needed if required by the struct definition - }, - ) - .collect()) + Ok(( + parsed + .into_iter() + .enumerate() + .map( + |(idx, parsed)| dynamo_async_openai::types::ChatCompletionMessageToolCallChunk { + index: idx as u32, + id: Some(parsed.id), + r#type: Some(dynamo_async_openai::types::ChatCompletionToolType::Function), + function: Some(dynamo_async_openai::types::FunctionCallStream { + name: Some(parsed.function.name), + arguments: Some(parsed.function.arguments), + }), + // Add other fields as needed if required by the struct definition + }, + ) + .collect(), + content, + )) } From 955ad8ea26ab7526c2892ab0191e10e27be48431 Mon Sep 17 00:00:00 2001 From: Michael Gathara Date: Tue, 26 Aug 2025 13:33:16 -0500 Subject: [PATCH 32/82] feat: HF_ENDPOINT addition (#2637) Signed-off-by: Jason Zhou --- .../operator/internal/dynamo/backend_trtllm.go | 2 +- .../internal/dynamo/backend_trtllm_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deploy/cloud/operator/internal/dynamo/backend_trtllm.go b/deploy/cloud/operator/internal/dynamo/backend_trtllm.go index 712fd49aa6..88cc8f8303 100644 --- a/deploy/cloud/operator/internal/dynamo/backend_trtllm.go +++ b/deploy/cloud/operator/internal/dynamo/backend_trtllm.go @@ -188,7 +188,7 @@ func getGPUsPerNode(resources *common.Resources) int32 { // getCommonTRTLLMEnvVars returns a map of common environment variables for TRTLLM deployments func getCommonTRTLLMEnvVars() map[string]bool { return map[string]bool{ - "CUDA_VISIBLE_DEVICES": true, "MODEL_PATH": true, "HF_TOKEN": true, "HUGGING_FACE_HUB_TOKEN": true, + "CUDA_VISIBLE_DEVICES": true, "MODEL_PATH": true, "HF_TOKEN": true, "HUGGING_FACE_HUB_TOKEN": true, "HF_ENDPOINT": true, "TOKENIZERS_PARALLELISM": true, "NCCL_DEBUG": true, "NCCL_IB_DISABLE": true, "NCCL_P2P_DISABLE": true, "TENSORRT_LLM_CACHE_DIR": true, "HF_HOME": true, "TRANSFORMERS_CACHE": true, "HF_DATASETS_CACHE": true, "PATH": true, "LD_LIBRARY_PATH": true, "PYTHONPATH": true, "HOME": true, "USER": true, diff --git a/deploy/cloud/operator/internal/dynamo/backend_trtllm_test.go b/deploy/cloud/operator/internal/dynamo/backend_trtllm_test.go index 69f70b38ce..c20ec6438c 100644 --- a/deploy/cloud/operator/internal/dynamo/backend_trtllm_test.go +++ b/deploy/cloud/operator/internal/dynamo/backend_trtllm_test.go @@ -60,7 +60,7 @@ func TestTRTLLMBackend_UpdateContainer(t *testing.T) { {Name: commonconsts.MpiRunSshSecretName, MountPath: "/ssh-pk", ReadOnly: true}, }, expectedCommand: []string{"/bin/sh", "-c"}, - expectedArgs: []string{"mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 6 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-1.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x OMPI_MCA_orte_keep_fqdn_hostnames -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python3 --model test'"}, + expectedArgs: []string{"mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 6 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-1.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_ENDPOINT -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x OMPI_MCA_orte_keep_fqdn_hostnames -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python3 --model test'"}, expectedEnv: []corev1.EnvVar{ {Name: "OMPI_MCA_orte_keep_fqdn_hostnames", Value: "1"}, }, @@ -116,7 +116,7 @@ func TestTRTLLMBackend_UpdateContainer(t *testing.T) { {Name: commonconsts.MpiRunSshSecretName, MountPath: "/ssh-pk", ReadOnly: true}, }, expectedCommand: []string{"/bin/sh", "-c"}, - expectedArgs: []string{"mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${LWS_LEADER_ADDRESS},${LWS_WORKER_1_ADDRESS} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x OMPI_MCA_orte_keep_fqdn_hostnames -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python3 --model test'"}, + expectedArgs: []string{"mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${LWS_LEADER_ADDRESS},${LWS_WORKER_1_ADDRESS} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_ENDPOINT -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x OMPI_MCA_orte_keep_fqdn_hostnames -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python3 --model test'"}, expectedEnv: []corev1.EnvVar{ {Name: "OMPI_MCA_orte_keep_fqdn_hostnames", Value: "1"}, }, @@ -563,7 +563,7 @@ func TestTRTLLMBackend_setupLeaderContainer(t *testing.T) { }, initialArgs: []string{"python3", "--model", "test"}, initialCommand: []string{}, - expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 6 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-1.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python3 --model test'", + expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 6 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-service-wkr-1.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_ENDPOINT -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python3 --model test'", }, { name: "Leader with command and no GPU resources", @@ -573,7 +573,7 @@ func TestTRTLLMBackend_setupLeaderContainer(t *testing.T) { component: &v1alpha1.DynamoComponentDeploymentOverridesSpec{}, initialArgs: []string{}, initialCommand: []string{"python", "-m", "worker"}, - expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 0 -H ${LWS_LEADER_ADDRESS},${LWS_WORKER_1_ADDRESS} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python -m worker'", + expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 0 -H ${LWS_LEADER_ADDRESS},${LWS_WORKER_1_ADDRESS} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_ENDPOINT -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch python -m worker'", }, { name: "Leader with both command and args (args take precedence)", @@ -591,7 +591,7 @@ func TestTRTLLMBackend_setupLeaderContainer(t *testing.T) { }, initialArgs: []string{"launch", "--config", "test.yaml"}, initialCommand: []string{"ignored-command"}, - expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-wkr-0.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch launch --config test.yaml'", + expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-wkr-0.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_ENDPOINT -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch launch --config test.yaml'", }, { name: "Leader with all environment variables forwarded", @@ -609,7 +609,7 @@ func TestTRTLLMBackend_setupLeaderContainer(t *testing.T) { }, initialArgs: []string{"serve", "--model", "test"}, initialCommand: []string{}, - expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-wkr-0.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch serve --model test'", + expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-wkr-0.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x HF_DATASETS_CACHE -x HF_ENDPOINT -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch serve --model test'", }, { name: "Leader with overlapping environment variables (deduplication test)", @@ -627,7 +627,7 @@ func TestTRTLLMBackend_setupLeaderContainer(t *testing.T) { }, initialArgs: []string{"serve", "--model", "test"}, initialCommand: []string{}, - expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-wkr-0.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x CUSTOM_VAR -x HF_DATASETS_CACHE -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch serve --model test'", + expected: "mkdir -p ~/.ssh && ls -la /ssh-pk/ && cp /ssh-pk/private.key ~/.ssh/id_rsa && cp /ssh-pk/private.key.pub ~/.ssh/id_rsa.pub && cp /ssh-pk/private.key.pub ~/.ssh/authorized_keys && chmod 600 ~/.ssh/id_rsa ~/.ssh/authorized_keys && chmod 644 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys && printf 'Host *\\nIdentityFile ~/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort 2222\\n' > ~/.ssh/config && mpirun --oversubscribe -n 2 -H ${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-ldr-0.${GROVE_HEADLESS_SERVICE},${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-test-wkr-0.${GROVE_HEADLESS_SERVICE} --mca pml ob1 --mca plm_rsh_args \"-p 2222 -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa\" -x CUDA_VISIBLE_DEVICES -x CUSTOM_VAR -x HF_DATASETS_CACHE -x HF_ENDPOINT -x HF_HOME -x HF_TOKEN -x HOME -x HUGGING_FACE_HUB_TOKEN -x LD_LIBRARY_PATH -x MODEL_PATH -x NCCL_DEBUG -x NCCL_IB_DISABLE -x NCCL_P2P_DISABLE -x PATH -x PYTHONPATH -x TENSORRT_LLM_CACHE_DIR -x TOKENIZERS_PARALLELISM -x TRANSFORMERS_CACHE -x USER bash -c 'source /opt/dynamo/venv/bin/activate && trtllm-llmapi-launch serve --model test'", }, } From 116c07cbbd8732f59a703a1f00f3e8e50fb42eea Mon Sep 17 00:00:00 2001 From: Yunzhou Liu <46603306+Elnifio@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:01:28 -0700 Subject: [PATCH 33/82] docs: Update how containers should be built for SGLang examples (#2707) Signed-off-by: Jason Zhou --- components/backends/sglang/docs/multinode-examples.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/backends/sglang/docs/multinode-examples.md b/components/backends/sglang/docs/multinode-examples.md index 1bab6f7cbf..c706b0946c 100644 --- a/components/backends/sglang/docs/multinode-examples.md +++ b/components/backends/sglang/docs/multinode-examples.md @@ -9,6 +9,15 @@ SPDX-License-Identifier: Apache-2.0 SGLang allows you to deploy multi-node sized models by adding in the `dist-init-addr`, `nnodes`, and `node-rank` arguments. Below we demonstrate and example of deploying DeepSeek R1 for disaggregated serving across 4 nodes. This example requires 4 nodes of 8xH100 GPUs. +**Prerequisite**: Building the Dynamo container. + +```bash +cd $DYNAMO_ROOT +docker build -f container/Dockerfile.sglang-wideep . -t dynamo-wideep --no-cache +``` + +You can use a specific tag from the [lmsys dockerhub](https://hub.docker.com/r/lmsysorg/sglang/tags) by adding `--build-arg SGLANG_IMAGE_TAG=` to the build command. + **Step 1**: Use the provided helper script to generate commands to start NATS/ETCD on your head prefill node. This script will also give you environment variables to export on each other node. You will need the IP addresses of your head prefill and head decode node to run this script. ```bash ./utils/gen_env_vars.sh From 6bdaebecdd78c72a4a0c63801b2643ec7174b615 Mon Sep 17 00:00:00 2001 From: Chi McIsaac <153383231+qimcis@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:34:15 -0400 Subject: [PATCH 34/82] feat: align OpenAI response IDs with distributed trace IDs (#2496) Signed-off-by: Jason Zhou --- lib/engines/mistralrs/src/lib.rs | 37 +++++++++-------- lib/llm/src/engines.rs | 5 +-- lib/llm/src/http/service/openai.rs | 13 +++--- lib/llm/src/preprocessor.rs | 9 +--- .../openai/chat_completions/delta.rs | 26 ++++++------ .../src/protocols/openai/completions/delta.rs | 22 +++------- lib/llm/tests/http-service.rs | 41 ++++++------------- 7 files changed, 60 insertions(+), 93 deletions(-) diff --git a/lib/engines/mistralrs/src/lib.rs b/lib/engines/mistralrs/src/lib.rs index ad819e648d..517edb7f78 100644 --- a/lib/engines/mistralrs/src/lib.rs +++ b/lib/engines/mistralrs/src/lib.rs @@ -212,9 +212,9 @@ impl MistralRsEngine { // Perform warmup request let (tx, mut rx) = channel(1); - let request_id = engine.mistralrs.next_request_id(); + let mistralrs_request_id = engine.mistralrs.next_request_id(); let warmup_request = Request::Normal(Box::new(NormalRequest { - id: request_id, + id: mistralrs_request_id, model_id: Some(display_name.to_string()), messages: RequestMessage::Chat { messages: vec![IndexMap::from([ @@ -246,10 +246,10 @@ impl MistralRsEngine { { match response.as_result() { Ok(r) => { - tracing::debug!(request_id, "Warmup response: {r:?}"); + tracing::debug!(mistralrs_request_id, "Warmup response: {r:?}"); } Err(err) => { - tracing::error!(request_id, %err, "Failed converting response to result."); + tracing::error!(mistralrs_request_id, %err, "Failed converting response to result."); } } } @@ -272,6 +272,7 @@ impl ) -> Result>, Error> { let (request, context) = request.transfer(()); let ctx = context.context(); + let request_id = ctx.id().to_string(); let (tx, mut rx) = channel(10_000); let mut messages = vec![]; @@ -338,9 +339,9 @@ impl n_choices: 1, dry_params: det.dry_params, }; - let request_id = self.mistralrs.next_request_id(); + let mistralrs_request_id = self.mistralrs.next_request_id(); let mistralrs_request = Request::Normal(Box::new(NormalRequest { - id: request_id, + id: mistralrs_request_id, model_id: Some(self.display_name.clone()), messages: RequestMessage::Chat { messages, @@ -369,14 +370,14 @@ impl let response = match response.as_result() { Ok(r) => r, Err(err) => { - tracing::error!(request_id, %err, "Failed converting mistralrs channel response to result."); + tracing::error!(mistralrs_request_id, %err, "Failed converting mistralrs channel response to result."); break; } }; match response { ResponseOk::Chunk(c) => { let Some(from_assistant) = c.choices[0].delta.content.clone() else { - tracing::warn!(request_id, "No content from mistralrs. Abandoning request."); + tracing::warn!(mistralrs_request_id, "No content from mistralrs. Abandoning request."); break; }; let finish_reason = match &c.choices[0].finish_reason.as_deref() { @@ -387,7 +388,7 @@ impl Some(FinishReason::Length) } Some(s) => { - tracing::warn!(request_id, stop_reason = s, "Unknow stop reason"); + tracing::warn!(mistralrs_request_id, stop_reason = s, "Unknow stop reason"); Some(FinishReason::Stop) } None => None, @@ -396,7 +397,7 @@ impl #[allow(deprecated)] let delta = NvCreateChatCompletionStreamResponse { - id: c.id, + id: format!("chatcmpl-{request_id}"), choices: vec![dynamo_async_openai::types::ChatChoiceStream{ index: 0, delta: dynamo_async_openai::types::ChatCompletionStreamResponseDelta{ @@ -427,11 +428,11 @@ impl yield ann; if finish_reason.is_some() { - //tracing::trace!(request_id, "Finish reason: {finish_reason:?}"); + //tracing::trace!(mistralrs_request_id, "Finish reason: {finish_reason:?}"); break; } }, - x => tracing::error!(request_id, "Unhandled. {x:?}"), + x => tracing::error!(mistralrs_request_id, "Unhandled. {x:?}"), } } }; @@ -485,7 +486,7 @@ impl let (request, context) = request.transfer(()); let ctx = context.context(); let (tx, mut rx) = channel(10_000); - let response_generator = request.response_generator(); + let response_generator = request.response_generator(ctx.id().to_string()); let messages = RequestMessage::Completion { text: prompt_to_string(&request.inner.prompt), @@ -539,9 +540,9 @@ impl dry_params: det.dry_params, }; - let request_id = self.mistralrs.next_request_id(); + let mistralrs_request_id = self.mistralrs.next_request_id(); let mistralrs_request = Request::Normal(Box::new(NormalRequest { - id: request_id, + id: mistralrs_request_id, model_id: Some(self.display_name.clone()), messages, sampling_params, @@ -567,7 +568,7 @@ impl let response = match response.as_result() { Ok(r) => r, Err(err) => { - tracing::error!(request_id, %err, "Failed converting mistralrs channel response to result."); + tracing::error!(mistralrs_request_id, %err, "Failed converting mistralrs channel response to result."); break; } }; @@ -583,7 +584,7 @@ impl Some(FinishReason::Length) } Some(s) => { - tracing::warn!(request_id, stop_reason = s, "Unknow stop reason"); + tracing::warn!(mistralrs_request_id, stop_reason = s, "Unknow stop reason"); Some(FinishReason::Stop) } None => None, @@ -602,7 +603,7 @@ impl break; } }, - x => tracing::error!(request_id, "Unhandled. {x:?}"), + x => tracing::error!(mistralrs_request_id, "Unhandled. {x:?}"), } } }; diff --git a/lib/llm/src/engines.rs b/lib/llm/src/engines.rs index 43fc3002fa..ce489f1d6f 100644 --- a/lib/llm/src/engines.rs +++ b/lib/llm/src/engines.rs @@ -14,7 +14,6 @@ use dynamo_runtime::pipeline::{Error, ManyOut, SingleIn}; use dynamo_runtime::protocols::annotated::Annotated; use crate::backend::ExecutionContext; -use crate::local_model::runtime_config; use crate::preprocessor::PreprocessedRequest; use crate::protocols::common::llm_backend::LLMEngineOutput; use crate::protocols::openai::{ @@ -184,8 +183,8 @@ impl incoming_request: SingleIn, ) -> Result>, Error> { let (request, context) = incoming_request.transfer(()); - let mut deltas = request.response_generator(runtime_config::ModelRuntimeConfig::default()); let ctx = context.context(); + let mut deltas = request.response_generator(ctx.id().to_string()); let req = request.inner.messages.into_iter().next_back().unwrap(); let prompt = match req { @@ -231,8 +230,8 @@ impl incoming_request: SingleIn, ) -> Result>, Error> { let (request, context) = incoming_request.transfer(()); - let deltas = request.response_generator(); let ctx = context.context(); + let deltas = request.response_generator(ctx.id().to_string()); let chars_string = prompt_to_string(&request.inner.prompt); let output = stream! { let mut id = 1; diff --git a/lib/llm/src/http/service/openai.rs b/lib/llm/src/http/service/openai.rs index d81877f6b7..b8fbc33ec6 100644 --- a/lib/llm/src/http/service/openai.rs +++ b/lib/llm/src/http/service/openai.rs @@ -253,8 +253,7 @@ async fn completions( // return a 503 if the service is not ready check_ready(&state)?; - // todo - extract distributed tracing id and context id from headers - let request_id = uuid::Uuid::new_v4().to_string(); + let request_id = request.id().to_string(); // todo - decide on default let streaming = request.inner.stream.unwrap_or(false); @@ -354,13 +353,15 @@ async fn completions( #[tracing::instrument(skip_all)] async fn embeddings( State(state): State>, + headers: HeaderMap, Json(request): Json, ) -> Result { // return a 503 if the service is not ready check_ready(&state)?; - // todo - extract distributed tracing id and context id from headers - let request_id = uuid::Uuid::new_v4().to_string(); + let request_id = get_or_create_request_id(request.inner.user.as_deref(), &headers); + let request = Context::with_id(request, request_id); + let request_id = request.id().to_string(); // Embeddings are typically not streamed, so we default to non-streaming let streaming = false; @@ -381,10 +382,6 @@ async fn embeddings( .metrics_clone() .create_inflight_guard(model, Endpoint::Embeddings, streaming); - // setup context - // todo - inherit request_id from distributed trace details - let request = Context::with_id(request, request_id.clone()); - // issue the generate call on the engine let stream = engine .generate(request) diff --git a/lib/llm/src/preprocessor.rs b/lib/llm/src/preprocessor.rs index 7c96f49a22..71fdb7515a 100644 --- a/lib/llm/src/preprocessor.rs +++ b/lib/llm/src/preprocessor.rs @@ -22,7 +22,6 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use std::{collections::HashMap, sync::Arc}; use tracing; -use crate::local_model::runtime_config::ModelRuntimeConfig; use crate::model_card::{ModelDeploymentCard, ModelInfo, TokenizerKind}; use crate::preprocessor::prompt::OAIChatLikeRequest; use crate::tokenizers::Encoding; @@ -95,7 +94,6 @@ pub struct OpenAIPreprocessor { formatter: Arc, tokenizer: Arc, model_info: Arc, - runtime_config: ModelRuntimeConfig, } impl OpenAIPreprocessor { @@ -123,14 +121,11 @@ impl OpenAIPreprocessor { }; let model_info = model_info.get_model_info().await?; - let runtime_config = mdc.runtime_config.clone(); - Ok(Arc::new(Self { formatter, tokenizer, model_info, mdcsum, - runtime_config, })) } @@ -499,7 +494,7 @@ impl let (request, context) = request.into_parts(); // create a response generator - let response_generator = request.response_generator(self.runtime_config.clone()); + let response_generator = request.response_generator(context.id().to_string()); let mut response_generator = Box::new(response_generator); // convert the chat completion request to a common completion request @@ -553,7 +548,7 @@ impl let (request, context) = request.into_parts(); // create a response generator - let response_generator = request.response_generator(); + let response_generator = request.response_generator(context.id().to_string()); let mut response_generator = Box::new(response_generator); // convert the chat completion request to a common completion request let (common_request, annotations) = self.preprocess_request(&request)?; diff --git a/lib/llm/src/protocols/openai/chat_completions/delta.rs b/lib/llm/src/protocols/openai/chat_completions/delta.rs index b64a8019bc..718d5da92b 100644 --- a/lib/llm/src/protocols/openai/chat_completions/delta.rs +++ b/lib/llm/src/protocols/openai/chat_completions/delta.rs @@ -1,33 +1,32 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use dynamo_parsers::{ParserResult, ReasoningParser, ReasoningParserType, ReasoningParserWrapper}; - use super::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse}; use crate::{ - local_model::runtime_config, + local_model::runtime_config::ModelRuntimeConfig, protocols::common::{self}, types::TokenIdType, }; +use dynamo_parsers::{ParserResult, ReasoningParser, ReasoningParserType, ReasoningParserWrapper}; /// Provides a method for generating a [`DeltaGenerator`] from a chat completion request. impl NvCreateChatCompletionRequest { /// Creates a [`DeltaGenerator`] instance based on the chat completion request. /// + /// # Arguments + /// * `request_id` - The request ID to use for the chat completion response ID. + /// /// # Returns /// * [`DeltaGenerator`] configured with model name and response options. - pub fn response_generator( - &self, - runtime_config: runtime_config::ModelRuntimeConfig, - ) -> DeltaGenerator { + pub fn response_generator(&self, request_id: String) -> DeltaGenerator { let options = DeltaGeneratorOptions { enable_usage: true, enable_logprobs: self.inner.logprobs.unwrap_or(false) || self.inner.top_logprobs.unwrap_or(0) > 0, - runtime_config, + runtime_config: ModelRuntimeConfig::default(), }; - DeltaGenerator::new(self.inner.model.clone(), options) + DeltaGenerator::new(self.inner.model.clone(), options, request_id) } } @@ -39,7 +38,7 @@ pub struct DeltaGeneratorOptions { /// Determines whether log probabilities should be included in the response. pub enable_logprobs: bool, - pub runtime_config: runtime_config::ModelRuntimeConfig, + pub runtime_config: ModelRuntimeConfig, } /// Generates incremental chat completion responses in a streaming fashion. @@ -74,10 +73,11 @@ impl DeltaGenerator { /// # Arguments /// * `model` - The model name used for response generation. /// * `options` - Configuration options for enabling usage and log probabilities. + /// * `request_id` - The request ID to use for the chat completion response. /// /// # Returns /// * A new instance of [`DeltaGenerator`]. - pub fn new(model: String, options: DeltaGeneratorOptions) -> Self { + pub fn new(model: String, options: DeltaGeneratorOptions, request_id: String) -> Self { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -108,8 +108,10 @@ impl DeltaGenerator { .unwrap_or("basic"), ); + let chatcmpl_id = format!("chatcmpl-{request_id}"); + Self { - id: format!("chatcmpl-{}", uuid::Uuid::new_v4()), + id: chatcmpl_id, object: "chat.completion.chunk".to_string(), created: now, model, diff --git a/lib/llm/src/protocols/openai/completions/delta.rs b/lib/llm/src/protocols/openai/completions/delta.rs index 0122cb21fe..6509879bc8 100644 --- a/lib/llm/src/protocols/openai/completions/delta.rs +++ b/lib/llm/src/protocols/openai/completions/delta.rs @@ -1,17 +1,5 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// -// Licensed 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. use super::{NvCreateCompletionRequest, NvCreateCompletionResponse}; use crate::{protocols::common, types::TokenIdType}; @@ -19,13 +7,13 @@ use crate::{protocols::common, types::TokenIdType}; impl NvCreateCompletionRequest { // put this method on the request // inspect the request to extract options - pub fn response_generator(&self) -> DeltaGenerator { + pub fn response_generator(&self, request_id: String) -> DeltaGenerator { let options = DeltaGeneratorOptions { enable_usage: true, enable_logprobs: self.inner.logprobs.unwrap_or(0) > 0, }; - DeltaGenerator::new(self.inner.model.clone(), options) + DeltaGenerator::new(self.inner.model.clone(), options, request_id) } } @@ -47,7 +35,7 @@ pub struct DeltaGenerator { } impl DeltaGenerator { - pub fn new(model: String, options: DeltaGeneratorOptions) -> Self { + pub fn new(model: String, options: DeltaGeneratorOptions, request_id: String) -> Self { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -67,8 +55,10 @@ impl DeltaGenerator { prompt_tokens_details: None, }; + let completion_id = format!("cmpl-{request_id}"); + Self { - id: format!("cmpl-{}", uuid::Uuid::new_v4()), + id: completion_id, object: "text_completion".to_string(), created: now, model, diff --git a/lib/llm/tests/http-service.rs b/lib/llm/tests/http-service.rs index 7bbaad2247..c21a311519 100644 --- a/lib/llm/tests/http-service.rs +++ b/lib/llm/tests/http-service.rs @@ -1,21 +1,20 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// -// Licensed 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. use anyhow::Error; use async_stream::stream; use dynamo_async_openai::config::OpenAIConfig; +use dynamo_llm::http::{ + client::{ + GenericBYOTClient, HttpClientConfig, HttpRequestContext, NvCustomClient, PureOpenAIClient, + }, + service::{ + Metrics, + error::HttpError, + metrics::{Endpoint, FRONTEND_METRIC_PREFIX, RequestType, Status}, + service_v2::HttpService, + }, +}; use dynamo_llm::protocols::{ Annotated, codec::SseLineCodec, @@ -25,21 +24,6 @@ use dynamo_llm::protocols::{ completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, }, }; -use dynamo_llm::{ - http::{ - client::{ - GenericBYOTClient, HttpClientConfig, HttpRequestContext, NvCustomClient, - PureOpenAIClient, - }, - service::{ - Metrics, - error::HttpError, - metrics::{Endpoint, FRONTEND_METRIC_PREFIX, RequestType, Status}, - service_v2::HttpService, - }, - }, - local_model::runtime_config, -}; use dynamo_runtime::{ CancellationToken, engine::AsyncEngineContext, @@ -99,8 +83,7 @@ impl let max_tokens = request.inner.max_tokens.unwrap_or(0) as u64; // let generator = NvCreateChatCompletionStreamResponse::generator(request.model.clone()); - let mut generator = - request.response_generator(runtime_config::ModelRuntimeConfig::default()); + let mut generator = request.response_generator(ctx.id().to_string()); let stream = stream! { tokio::time::sleep(std::time::Duration::from_millis(max_tokens)).await; From 8b16a5fd5f1bb61b462e69855e3b8b4832281c5e Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:35:56 -0600 Subject: [PATCH 35/82] fix: fix sglang multinode example (#2716) Signed-off-by: Jason Zhou --- components/backends/sglang/deploy/disagg-multinode.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/backends/sglang/deploy/disagg-multinode.yaml b/components/backends/sglang/deploy/disagg-multinode.yaml index 2817cf7dec..6635be8d0c 100644 --- a/components/backends/sglang/deploy/disagg-multinode.yaml +++ b/components/backends/sglang/deploy/disagg-multinode.yaml @@ -18,7 +18,7 @@ spec: services: Frontend: dynamoNamespace: sglang-disagg-multinode - componentType: main + componentType: frontend replicas: 1 extraPodSpec: mainContainer: @@ -54,7 +54,7 @@ spec: multinode: nodeCount: 2 envFromSecret: hf-token-secret - dynamoNamespace: sglang-disagg + dynamoNamespace: sglang-disagg-multinode componentType: worker replicas: 1 resources: From 49815adb8b5605361a5e9e8f8631778db584e0a5 Mon Sep 17 00:00:00 2001 From: mohammedabdulwahhab Date: Tue, 26 Aug 2025 13:14:37 -0700 Subject: [PATCH 36/82] fix: fix metrics docs; add dcgm-exporter (#2712) Signed-off-by: mohammedabdulwahhab Co-authored-by: Ryan McCormick Signed-off-by: Jason Zhou --- docs/guides/dynamo_deploy/k8s_metrics.md | 43 +++++++++++++++++++----- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/docs/guides/dynamo_deploy/k8s_metrics.md b/docs/guides/dynamo_deploy/k8s_metrics.md index e6dc9e7a6e..1e74400b0d 100644 --- a/docs/guides/dynamo_deploy/k8s_metrics.md +++ b/docs/guides/dynamo_deploy/k8s_metrics.md @@ -2,15 +2,15 @@ ## Overview -This guide provides a walkthrough for collecting and visualizing metrics from Dynamo components using the Prometheus Operator stack. The Prometheus Operator provides a powerful and flexible way to configure monitoring for Kubernetes applications through custom resources like PodMonitors, making it easy to automatically discover and scrape metrics from Dynamo components. +This guide provides a walkthrough for collecting and visualizing metrics from Dynamo components using the kube-prometheus-stack. The kube-prometheus-stack provides a powerful and flexible way to configure monitoring for Kubernetes applications through custom resources like PodMonitors, making it easy to automatically discover and scrape metrics from Dynamo components. ## Prerequisites ### Install Dynamo Operator Before setting up metrics collection, you'll need to have the Dynamo operator installed in your cluster. Follow our [Installation Guide](../dynamo_deploy/dynamo_cloud.md) for detailed instructions on deploying the Dynamo operator. -### Install Prometheus Operator -If you don't have an existing Prometheus setup, you'll need to install the Prometheus Operator. The Prometheus Operator introduces custom resources that make it easy to deploy and manage Prometheus monitoring in Kubernetes: +### Install kube-prometheus-stack +If you don't have an existing Prometheus setup, you'll likely want to install the kube-prometheus-stack. This is a collection of Kubernetes manifests that includes the Prometheus Operator, Prometheus, Grafana, and other monitoring components in a pre-configured setup. The stack introduces custom resources that make it easy to deploy and manage monitoring in Kubernetes: - `PodMonitor`: Automatically discovers and scrapes metrics from pods based on label selectors - `ServiceMonitor`: Similar to PodMonitor but works with Services @@ -20,9 +20,27 @@ For a basic installation: ```bash helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update -helm install prometheus prometheus-community/kube-prometheus-stack +# Values allow podmnonitors to be picked up that are outside of the kube-prometheus-stack helm release +helm install prometheus prometheus-community/kube-prometheus-stack \ + --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ + --set prometheus.prometheusSpec.podMonitorNamespaceSelector="{}" \ + --set prometheus.prometheusSpec.probeNamespaceSelector="{}" ``` +> [!Note] +> The commands enumerated below assume you have installed the kube-prometheus-stack with the installation method listed above. Depending on your installation configuration of the monitoring stack, you may need to modify the `kubectl` commands that follow in this document accordingly (e.g modifying Namespace or Service names accordingly). + +### DCGM Metrics Collection (Optional) + +GPU utilization metrics are collected and exported to Prometheus via dcgm-exporter. The Dynamo Grafana dashboard includes a panel for GPU utilization related to your Dynamo deployment. For that panel to be populated, you need to ensure that the dcgm-exporter is running in your cluster. To check if the dcgm-exporter is running, please run the following command: + +```bash +kubectl get daemonset -A | grep dcgm-exporter +``` + +If the output is empty, you need to install the dcgm-exporter. For more information, please consult the official [dcgm-exporter documentation](https://docs.nvidia.com/datacenter/cloud-native/gpu-telemetry/latest/dcgm-exporter.html). + + ## Deploy a DynamoGraphDeployment Let's start by deploying a simple vLLM aggregated deployment: @@ -178,13 +196,13 @@ The dashboard is embedded in the ConfigMap. Since it is labeled with `grafana_da - Inter-token latency - Request duration - Input/Output sequence lengths -- GPU utilization +- GPU utilization via DCGM ## Viewing the Metrics ### In Prometheus ```bash -kubectl port-forward svc/prometheus-operated 9090:9090 -n monitoring +kubectl port-forward svc/prometheus-kube-prometheus-prometheus 9090:9090 -n monitoring ``` Visit http://localhost:9090 and try these example queries: @@ -195,9 +213,18 @@ Visit http://localhost:9090 and try these example queries: ### In Grafana ```bash -kubectl port-forward svc/grafana 3000:80 -n monitoring +# Get Grafana credentials +export GRAFANA_USER=$(kubectl get secret -n monitoring prometheus-grafana -o jsonpath="{.data.admin-user}" | base64 --decode) +export GRAFANA_PASSWORD=$(kubectl get secret -n monitoring prometheus-grafana -o jsonpath="{.data.admin-password}" | base64 --decode) +echo "Grafana user: $GRAFANA_USER" +echo "Grafana password: $GRAFANA_PASSWORD" + +# Port forward Grafana service +kubectl port-forward svc/prometheus-grafana 3000:80 -n monitoring ``` -Visit http://localhost:3000 and find the Dynamo dashboard under General. +Visit http://localhost:3000 and log in with the credentials captured above. + +Once logged in, find the Dynamo dashboard under General. ![Grafana dashboard showing Dynamo metrics](../../images/grafana-k8s.png) From bfd1fc7534625798a7499ee95f7f7246b56a3389 Mon Sep 17 00:00:00 2001 From: hhzhang16 <54051230+hhzhang16@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:54:30 -0400 Subject: [PATCH 37/82] fix: sglang -- queue requests until model registration completes (#2701) Signed-off-by: Hannah Zhang Signed-off-by: Jason Zhou --- .../backends/sglang/src/dynamo/sglang/main.py | 32 ++++++++++++++++--- .../sglang/src/dynamo/sglang/register.py | 12 +++++-- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/components/backends/sglang/src/dynamo/sglang/main.py b/components/backends/sglang/src/dynamo/sglang/main.py index 0fb1ce5408..604320b43c 100644 --- a/components/backends/sglang/src/dynamo/sglang/main.py +++ b/components/backends/sglang/src/dynamo/sglang/main.py @@ -81,18 +81,40 @@ async def init(runtime: DistributedRuntime, config: Config): logging.info(f"Setting up ZMQ kv event publisher at {zmq_ep}") kv_publisher = ZmqKvEventPublisher(component=component, config=zmq_config) + # Readiness gate: requests wait until model is registered + ready_event = asyncio.Event() + + async def gated_generate(request): + """Queue requests until model registration completes""" + await ready_event.wait() # Block until model is ready + async for response in handler.generate(request): + yield response + handler = DecodeWorkerHandler( component, engine, config, publisher, kv_publisher, prefill_client ) - await register_llm_with_runtime_config( - engine, generate_endpoint, server_args, dynamo_args.migration_limit - ) + async def register_model(): + """Register the model and signal readiness""" + registration_success = await register_llm_with_runtime_config( + engine, generate_endpoint, server_args, dynamo_args.migration_limit + ) + + if not registration_success: + logging.error("Model registration failed; shutting down") + runtime.shutdown() + raise RuntimeError("Model registration failed") + + # Model is ready - allow queued requests to proceed + ready_event.set() + logging.info("Model registration succeeded; processing queued requests") try: - # TODO: add in native endpoints + # Start endpoint immediately and register model concurrently + # Requests queue until ready_event is set await asyncio.gather( - generate_endpoint.serve_endpoint(handler.generate, graceful_shutdown=False), + generate_endpoint.serve_endpoint(gated_generate, graceful_shutdown=False), + register_model(), ) except Exception as e: logging.error(f"Failed to serve endpoints: {e}") diff --git a/components/backends/sglang/src/dynamo/sglang/register.py b/components/backends/sglang/src/dynamo/sglang/register.py index b9be9c16ed..cd7a24d761 100644 --- a/components/backends/sglang/src/dynamo/sglang/register.py +++ b/components/backends/sglang/src/dynamo/sglang/register.py @@ -16,8 +16,12 @@ async def register_llm_with_runtime_config( endpoint: Endpoint, server_args: ServerArgs, migration_limit: int, -): - """Register LLM with runtime config""" +) -> bool: + """Register LLM with runtime config + + Returns: + bool: True if registration succeeded, False if it failed + """ runtime_config = await _get_runtime_config(engine) try: await register_llm( @@ -29,9 +33,11 @@ async def register_llm_with_runtime_config( migration_limit=migration_limit, runtime_config=runtime_config, ) + logging.info("Successfully registered LLM with runtime config") + return True except Exception as e: logging.error(f"Failed to register with runtime config: {e}") - return None + return False async def _get_runtime_config(engine: sgl.Engine) -> Optional[ModelRuntimeConfig]: From b9029f701558891abb0d7d118f4da99226170929 Mon Sep 17 00:00:00 2001 From: Kris Hung Date: Tue, 26 Aug 2025 14:18:39 -0700 Subject: [PATCH 38/82] feat: Add vllm multimodal qwen aggregated support (#2694) Signed-off-by: Jason Zhou --- examples/multimodal/README.md | 6 +- .../multimodal/components/encode_worker.py | 77 ++++------ examples/multimodal/components/worker.py | 60 ++++---- examples/multimodal/utils/encode_utils.py | 132 ++++++++++++++++++ examples/multimodal/utils/image_loader.py | 4 +- examples/multimodal/utils/model.py | 78 ++++------- examples/multimodal/utils/protocol.py | 14 +- tests/serve/test_vllm.py | 18 ++- 8 files changed, 242 insertions(+), 147 deletions(-) create mode 100644 examples/multimodal/utils/encode_utils.py diff --git a/examples/multimodal/README.md b/examples/multimodal/README.md index 6b7963cecd..fbadd97001 100644 --- a/examples/multimodal/README.md +++ b/examples/multimodal/README.md @@ -59,12 +59,14 @@ flowchart LR pd_worker --> encode_worker ``` -***Note*** Only the LLaVA 1.5 7B model is supported. Qwen2.5-VL and Phi3V support will be added in the future. +***Note*** Aggregated serving supports LLaVA 1.5 7B and Qwen2.5-VL-7B-Instruct today. Phi3V support will be added in the future. Disaggregated serving is currently only confirmed for LLaVA (see note below). ```bash cd $DYNAMO_HOME/examples/multimodal # Serve a LLaVA 1.5 7B model: bash launch/agg.sh --model llava-hf/llava-1.5-7b-hf +# Serve a Qwen2.5-VL model: +bash launch/agg.sh --model Qwen/Qwen2.5-VL-7B-Instruct ``` ### Client @@ -98,6 +100,8 @@ curl http://localhost:8080/v1/chat/completions \ }' ``` +If serving the example Qwen model, replace `"llava-hf/llava-1.5-7b-hf"` in the `"model"` field with `"Qwen/Qwen2.5-VL-7B-Instruct"`. + You should see a response similar to this: ```json {"id": "c37b946e-9e58-4d54-88c8-2dbd92c47b0c", "object": "chat.completion", "created": 1747725277, "model": "llava-hf/llava-1.5-7b-hf", "choices": [{"index": 0, "message": {"role": "assistant", "content": " In the image, there is a city bus parked on a street, with a street sign nearby on the right side. The bus appears to be stopped out of service. The setting is in a foggy city, giving it a slightly moody atmosphere."}, "finish_reason": "stop"}]} diff --git a/examples/multimodal/components/encode_worker.py b/examples/multimodal/components/encode_worker.py index 904434c33f..09c222199a 100644 --- a/examples/multimodal/components/encode_worker.py +++ b/examples/multimodal/components/encode_worker.py @@ -21,9 +21,8 @@ import sys from typing import AsyncIterator, Tuple -import torch import uvloop -from transformers import AutoImageProcessor, LlavaForConditionalGeneration +from transformers import AutoImageProcessor from vllm.engine.arg_utils import AsyncEngineArgs from vllm.utils import FlexibleArgumentParser @@ -33,7 +32,9 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) from utils.args import Config, base_parse_args, parse_endpoint +from utils.encode_utils import encode_image_embeddings, get_encoder_components from utils.image_loader import ImageLoader +from utils.model import load_vision_model from utils.protocol import MyRequestOutput, vLLMMultimodalRequest configure_dynamo_logging() @@ -70,13 +71,14 @@ def __init__( self.image_processor = AutoImageProcessor.from_pretrained( self.model, trust_remote_code=True ) - # self.vision_model = load_vision_model(self.model) - self.vision_model = LlavaForConditionalGeneration.from_pretrained( - self.model, device_map="auto", torch_dtype=torch.float16 - ).eval() - + self.vision_model = load_vision_model(self.model) self.min_workers = 1 + # Get encoder components for the model + self.vision_encoder, self.projector = get_encoder_components( + self.model, self.vision_model + ) + def cleanup(self): pass @@ -108,49 +110,26 @@ async def generate( logger.debug(f"Processing image for request: {{ id: {request_id} }}") image_embeds = self.image_processor(images=image, return_tensors="pt") - # [gluo NOTE] The commented section is for VLM generalization support, - # will use more generic approach once utils/model.py is fixed, - # see utils/models.py for details. - # # Add a batch dimension to everything - # for item in image_embeds: - # image_embeds[item] = image_embeds[item].unsqueeze(0).to(DEVICE) - # logger.debug(f"Image embeds: {image_embeds}") - - # image_grid_thw = ( - # image_embeds["image_grid_thw"].tolist() - # if "image_grid_thw" in image_embeds - # else None - # ) - # image_sizes = ( - # image_embeds["image_sizes"].tolist() - # if "image_sizes" in image_embeds - # else [image.size] - # ) - # logger.debug( - # f"Pixel values stats: mean={image_embeds['pixel_values'].mean().item()}, std={image_embeds['pixel_values'].std().item()}, min={image_embeds['pixel_values'].min().item()}, max={image_embeds['pixel_values'].max().item()}" - # ) - - # with torch.no_grad(): - # embeddings = self.vision_model.get_multimodal_embeddings(**image_embeds) - # if isinstance(embeddings, tuple) or isinstance(embeddings, list): - # # The result multimodal_embeddings may be a list or tuple of tensors, with each - # # tensor corresponding to a multimodal data item (image or video). - # # TODO: for multi-image support, this result will contain multiple tensors. - # embeddings = embeddings[0].unsqueeze(0) - # logger.debug( - # f"Embeddings: {{ shape: {embeddings.shape}, dtype: {embeddings.dtype}, device: {embeddings.device}, ptr: {embeddings.data_ptr()}, elements: {{ count: {embeddings.numel()}, size: {embeddings.element_size()} }} }}." - # ) - - with torch.no_grad(): - logger.debug(f"Vision model device: {self.vision_model.device}") - vision_outputs = self.vision_model.vision_tower( - image_embeds["pixel_values"].to(self.vision_model.device) - ) - logger.debug("Vision model completed.") - - embeddings = vision_outputs.last_hidden_state - embeddings = self.vision_model.multi_modal_projector(embeddings) + # Encode the image embeddings using model-specific encoder + embeddings = encode_image_embeddings( + model_name=self.model, + image_embeds=image_embeds, + vision_encoder=self.vision_encoder, + projector=self.projector, + ) + + image_grid_thw = ( + image_embeds["image_grid_thw"].tolist() + if "image_grid_thw" in image_embeds + else None + ) + logger.debug( + f"Pixel values stats: mean={image_embeds['pixel_values'].mean().item()}, std={image_embeds['pixel_values'].std().item()}, min={image_embeds['pixel_values'].min().item()}, max={image_embeds['pixel_values'].max().item()}" + ) + + request.image_grid_thw = image_grid_thw + request.embeddings_shape = tuple(embeddings.shape) descriptor = connect.Descriptor(embeddings) with self._connector.create_readable(descriptor) as readable: diff --git a/examples/multimodal/components/worker.py b/examples/multimodal/components/worker.py index 5b0a9faf95..a549088158 100644 --- a/examples/multimodal/components/worker.py +++ b/examples/multimodal/components/worker.py @@ -24,7 +24,6 @@ import torch import uvloop -from transformers import AutoImageProcessor from vllm.distributed.kv_events import ZmqEventPublisher from vllm.engine.arg_utils import AsyncEngineArgs from vllm.inputs.data import TokensPrompt @@ -47,6 +46,7 @@ parse_endpoint, ) from utils.image_loader import ImageLoader +from utils.model import construct_mm_data from utils.protocol import MyRequestOutput, vLLMMultimodalRequest configure_dynamo_logging() @@ -245,37 +245,15 @@ async def async_init(self, runtime: DistributedRuntime): .client() ) - EMBEDDINGS_DTYPE = torch.float16 - EMBEDDINGS_DEVICE = "cpu" + self.EMBEDDINGS_DTYPE = torch.float16 + self.EMBEDDINGS_DEVICE = "cpu" # Create and initialize a dynamo connector for this worker. # We'll needs this to move data between this worker and remote workers efficiently. parsed_namespace, _, _ = parse_endpoint(self.endpoint) self._connector = connect.Connector() await self._connector.initialize() - # embeddings_shape, self.embeddings_dtype = get_vision_embeddings_info( - # self.engine_args.model, self.engine_args.num_patches - # ) - # [gluo NOTE] Hardcoded for now, will use more generic approach once utils/model.py - # is fixed, see utils/models.py for details. - embeddings_shape = (1, 577, 4096) - logger.debug(f"Embeddings shape: {embeddings_shape}") - self.embedding_size = embeddings_shape[1] - - embeddings = torch.empty( - embeddings_shape, dtype=EMBEDDINGS_DTYPE, device=EMBEDDINGS_DEVICE - ) - - descriptor = connect.Descriptor(embeddings) - - # Register the descriptor w/ NIXL (this is optional, if not done here the connect subsytem will take care of this automatically). - # descriptor.register_memory(self._connector) - self._embeddings_descriptor = (embeddings, descriptor) - self.image_loader = ImageLoader() - self.image_processor = AutoImageProcessor.from_pretrained( - self.engine_args.model, trust_remote_code=True - ) logger.info("VllmPDWorker has been initialized") @@ -288,10 +266,18 @@ async def generate(self, request: vLLMMultimodalRequest): request = vLLMMultimodalRequest.model_validate(request) logger.debug(f"Received PD request: {{ id: {request.request_id} }}.") - if request.image_url is None: - # Process embeddings using the connector - embeddings, descriptor = self._embeddings_descriptor + embeddings, descriptor = None, None + + # Process embeddings using the connector + # Create a descriptor based on the embedding shape. + embeddings = torch.empty( + request.embeddings_shape, + dtype=self.EMBEDDINGS_DTYPE, + device=self.EMBEDDINGS_DEVICE, + ) + descriptor = connect.Descriptor(embeddings) + if request.image_url is None: if descriptor is None: raise RuntimeError( "Descriptor is None in PD worker - cannot process embeddings" @@ -301,15 +287,17 @@ async def generate(self, request: vLLMMultimodalRequest): request.serialized_request, descriptor ) await read_op.wait_for_completion() - logger.debug(f"in PD worker, image features: {embeddings}") - multi_modal_data = embeddings + multi_modal_data = construct_mm_data( + self.engine_args.model, + embeddings, + self.EMBEDDINGS_DTYPE, + request.image_grid_thw, + ) else: # Use PIL image instead of image embeddings - multi_modal_data = await self.image_loader.load_image(request.image_url) - # multi_modal_data = self.image_processor(images=image, return_tensors="pt")["pixel_values"].to(dtype=torch.float16) - # image input is expected to be (image_num, channel, height, width) - # logger.info(f"Image features shape: {multi_modal_data.shape}") - # multi_modal_data = multi_modal_data.unsqueeze(0) + multi_modal_data = { + "image": await self.image_loader.load_image(request.image_url) + } # Remove the image features from the request as they are not required request.image_url = None @@ -331,7 +319,7 @@ async def generate(self, request: vLLMMultimodalRequest): gen = self.engine_client.generate( prompt=TokensPrompt( prompt_token_ids=pd_request.engine_prompt["prompt_token_ids"], - multi_modal_data={"image": multi_modal_data}, + multi_modal_data=multi_modal_data, ), sampling_params=pd_request.sampling_params, request_id=pd_request.request_id, diff --git a/examples/multimodal/utils/encode_utils.py b/examples/multimodal/utils/encode_utils.py new file mode 100644 index 0000000000..0b0f97efaf --- /dev/null +++ b/examples/multimodal/utils/encode_utils.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +import logging +from typing import Any, Dict, Optional + +import torch + +from .model import SupportedModels + +logger = logging.getLogger(__name__) + + +def get_qwen_image_features( + vision_encoder: torch.nn.Module, image_embeds: Dict[str, Any] +) -> torch.Tensor: + """ + Extract image features using Qwen-style vision encoder. + + Args: + vision_encoder: The vision encoder model + image_embeds: Dictionary containing pixel values and grid information + + Returns: + Processed image features tensor + + Raises: + ValueError: If grid_thw is not provided for Qwen model + """ + pixel_values = image_embeds["pixel_values"].to(vision_encoder.device) + + grid_thw = image_embeds.get("image_grid_thw", None) + if grid_thw is not None: + grid_thw = grid_thw.to(vision_encoder.device) + logger.debug(f"Qwen grid_thw shape: {grid_thw.shape}") + else: + raise ValueError("grid_thw is not provided") + + return ( + vision_encoder.get_image_features(pixel_values, grid_thw) # type: ignore + if grid_thw is not None + else vision_encoder.get_image_features(pixel_values) # type: ignore + ) + + +def encode_image_embeddings( + model_name: str, + image_embeds: Dict[str, Any], + vision_encoder: torch.nn.Module, + projector: Optional[torch.nn.Module] = None, +) -> torch.Tensor: + """ + Encode image embeddings using the appropriate model-specific encoder. + + Args: + model_name: The model identifier + image_embeds: Dictionary containing processed image data + vision_encoder: The vision encoder module + projector: The multimodal projector (required for LLaVA-style models) + + Returns: + Encoded embeddings tensor with normalized shape + + Raises: + ValueError: If projector is missing for LLaVA models + NotImplementedError: If model is not supported + """ + with torch.no_grad(): + # Route through the correct encoder based on model + if model_name == SupportedModels.LLAVA_1_5_7B: + pixel_values = image_embeds["pixel_values"].to(vision_encoder.device) + vision_outputs = vision_encoder(pixel_values) + + if projector is None: + raise ValueError(f"Projector not found for LLaVA model: {model_name}") + + embeddings = projector(vision_outputs.last_hidden_state) + + elif model_name == SupportedModels.QWEN_2_5_VL_7B: + embeddings = get_qwen_image_features(vision_encoder, image_embeds) + + else: + raise NotImplementedError(f"Model not supported: {model_name}") + + # Normalize output shape + if isinstance(embeddings, (tuple, list)): + embeddings = embeddings[0] + embeddings = embeddings.unsqueeze(0) if embeddings.ndim == 2 else embeddings + + return embeddings + + +def get_encoder_components( + model_name: str, vision_model: torch.nn.Module +) -> tuple[Any, Optional[Any]]: + """ + Get the appropriate vision encoder and projector components for a given model. + + Args: + model_name: The model identifier + vision_model: The loaded vision model + + Returns: + Tuple of (vision_encoder, projector) where types depend on the model + + Raises: + NotImplementedError: If model is not supported + """ + if model_name == SupportedModels.LLAVA_1_5_7B: + vision_encoder = vision_model.vision_tower + projector = getattr(vision_model, "multi_modal_projector", None) + return vision_encoder, projector + + elif model_name == SupportedModels.QWEN_2_5_VL_7B: + vision_encoder = vision_model + projector = None + return vision_encoder, projector + + else: + raise NotImplementedError(f"Model not supported: {model_name}") diff --git a/examples/multimodal/utils/image_loader.py b/examples/multimodal/utils/image_loader.py index 403d002151..fa313a65df 100644 --- a/examples/multimodal/utils/image_loader.py +++ b/examples/multimodal/utils/image_loader.py @@ -31,7 +31,9 @@ class ImageLoader: def __init__(self, cache_size: int = CACHE_SIZE_MAXIMUM): self._http_timeout = 30.0 - self._http_client = httpx.AsyncClient(timeout=self._http_timeout) + self._http_client = httpx.AsyncClient( + timeout=self._http_timeout, follow_redirects=True + ) self._image_cache: dict[str, Image.Image] = {} self._cache_queue: asyncio.Queue[str] = asyncio.Queue(maxsize=cache_size) diff --git a/examples/multimodal/utils/model.py b/examples/multimodal/utils/model.py index 3f7338f1b6..370b8039cd 100644 --- a/examples/multimodal/utils/model.py +++ b/examples/multimodal/utils/model.py @@ -14,61 +14,47 @@ # limitations under the License. import logging -from typing import Any, Dict, Tuple +from typing import Any, Dict, List, Optional, Tuple import torch -from transformers import AutoConfig -from utils.protocol import EncodeResponse -from vllm import AsyncEngineArgs -from vllm.utils import get_distributed_init_method, get_ip, get_open_port -from vllm.worker.worker import Worker +from transformers import AutoConfig, AutoModel -# from transformers import AutoImageProcessor, LlavaForConditionalGeneration -# from transformers import Qwen2_5_VLForConditionalGeneration, AutoTokenizer, AutoProcessor +logger = logging.getLogger(__name__) -logger = logging.getLogger(__name__) +class SupportedModels: + """Supported multimodal model identifiers""" + + LLAVA_1_5_7B = "llava-hf/llava-1.5-7b-hf" + QWEN_2_5_VL_7B = "Qwen/Qwen2.5-VL-7B-Instruct" + LLAVA_NEXT_VIDEO_7B = "llava-hf/LLaVA-NeXT-Video-7B-hf" -# [gluo NOTE] in vLLM v1, Worker() usage below will results in NotImplementedError, -# must find another way to properly load the vision model given the model name (model_id). def load_vision_model(model_id: str) -> torch.nn.Module: """ Load a vision model from a HuggingFace model ID. """ - engine_args = AsyncEngineArgs(model=model_id, trust_remote_code=True) - - engine_config = engine_args.create_engine_config() - distributed_init_method = get_distributed_init_method(get_ip(), get_open_port()) - worker = Worker( - vllm_config=engine_config, - local_rank=0, - rank=0, - distributed_init_method=distributed_init_method, - is_driver_worker=True, + model = AutoModel.from_pretrained( + model_id, device_map="auto", torch_dtype=torch.float16, trust_remote_code=True ) - # Initialize the worker. - worker.init_device() - worker.load_model() - return worker.model_runner.model - # model = LlavaForConditionalGeneration.from_pretrained( - # model_id, device_map="auto", torch_dtype=torch.float16 - # ).eval() - - # model = Qwen2_5_VLForConditionalGeneration.from_pretrained( - # model_id, torch_dtype="auto", device_map="auto" - # ).eval() - # return model + return model def get_vision_embeddings_info( - model_id: str, num_patches: int + model_id: str, ) -> Tuple[Tuple[int, int, int], torch.dtype]: """Calculate vision embeddings size and dtype using model config - Returns a tuple of (batch_size, num_patches, hidden_dim), dtype. + Returns a tuple of (batch_size, seq_len, hidden_dim), dtype. """ config = AutoConfig.from_pretrained(model_id, trust_remote_code=True) - assert num_patches > 0, "Number of patches must be positive" + + if model_id == SupportedModels.LLAVA_1_5_7B: + seq_len = 577 + elif model_id == SupportedModels.QWEN_2_5_VL_7B: + seq_len = 345 + else: + seq_len = 0 + if not hasattr(config, "torch_dtype"): raise ValueError("Model config missing required 'torch_dtype' attribute") if not hasattr(config, "hidden_size"): @@ -78,29 +64,27 @@ def get_vision_embeddings_info( hidden_size = 4096 else: hidden_size = config.hidden_size - return (1, num_patches, hidden_size), config.torch_dtype + return (1, seq_len, hidden_size), config.torch_dtype def construct_mm_data( model: str, - encode_output: EncodeResponse, image_embeds: torch.Tensor, embeddings_dtype: torch.dtype, + image_grid_thw: Optional[List[Any]], ) -> Dict[str, torch.Tensor | Dict[str, Any]]: """Construct multimodal data for a vLLM request for models that require additional parameters alongside the embeddings""" image_embeds = image_embeds.to(embeddings_dtype) - if "Qwen2" in model: + if model == SupportedModels.QWEN_2_5_VL_7B: + if image_grid_thw is not None and len(image_grid_thw) > 0: + grid_thw_tensor = torch.tensor(image_grid_thw) + else: + raise ValueError("No image grid provided.") + return { "image": { "image_embeds": image_embeds.squeeze(0), - "image_grid_thw": torch.tensor(encode_output.image_grid_thw).squeeze(0), - } - } - elif "MiniCPM-V" in model: - return { - "image": { - "image_embeds": image_embeds, - "image_sizes": encode_output.image_sizes, + "image_grid_thw": grid_thw_tensor, } } else: diff --git a/examples/multimodal/utils/protocol.py b/examples/multimodal/utils/protocol.py index f5083cc441..15e66f09c0 100644 --- a/examples/multimodal/utils/protocol.py +++ b/examples/multimodal/utils/protocol.py @@ -15,7 +15,7 @@ import json -from typing import Any, List, Literal, Optional, Union +from typing import Any, List, Literal, Optional, Tuple, Union import msgspec from pydantic import BaseModel, ConfigDict, field_validator @@ -127,7 +127,8 @@ class MultiModalRequest(BaseModel): class vLLMMultimodalRequest(vLLMGenerateRequest): model_config = ConfigDict(arbitrary_types_allowed=True) image_url: Optional[str] = None - # image_features: Optional[List[List[List[float]]]] = None # Remove once have NIXL support + image_grid_thw: Optional[List[Any]] = None + embeddings_shape: Optional[Tuple[int, int, int]] = None serialized_request: Optional[connect.RdmaMetadata] = None @@ -142,15 +143,6 @@ class EncodeRequest(BaseModel): serialized_request: Optional[connect.RdmaMetadata] = None -class EncodeResponse(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True) - request_id: str - image_grid_thw: Optional[List[Any]] = None - image_sizes: Optional[List[Any]] = None - serialized_request: Optional[connect.RdmaMetadata] = None - image_features: List[List[List[float]]] # Remove once have NIXL support - - class MyRequestOutput(BaseModel): """ RequestOutput from vLLM is not serializable by default diff --git a/tests/serve/test_vllm.py b/tests/serve/test_vllm.py index 31ec74ab4e..0d3363c420 100644 --- a/tests/serve/test_vllm.py +++ b/tests/serve/test_vllm.py @@ -166,8 +166,8 @@ def __init__(self, config: VLLMConfig, request): ], timeout=560, ), - "multimodal_agg": VLLMConfig( - name="multimodal_agg", + "multimodal_agg_llava": VLLMConfig( + name="multimodal_agg_llava", directory="/workspace/examples/multimodal", script_name="agg.sh", marks=[pytest.mark.gpu_2, pytest.mark.vllm], @@ -180,6 +180,20 @@ def __init__(self, config: VLLMConfig, request): args=["--model", "llava-hf/llava-1.5-7b-hf"], timeout=360, ), + "multimodal_agg_qwen": VLLMConfig( + name="multimodal_agg_qwen", + directory="/workspace/examples/multimodal", + script_name="agg.sh", + marks=[pytest.mark.gpu_2, pytest.mark.vllm], + endpoints=["v1/chat/completions"], + response_handlers=[ + chat_completions_response_handler, + ], + model="Qwen/Qwen2.5-VL-7B-Instruct", + delayed_start=0, + args=["--model", "Qwen/Qwen2.5-VL-7B-Instruct"], + timeout=360, + ), # TODO: Enable this test case when we have 4 GPUs runners. # "multimodal_disagg": VLLMConfig( # name="multimodal_disagg", From 04e0601557341e8a25182172e12a971ef31b2a01 Mon Sep 17 00:00:00 2001 From: nachiketb-nvidia Date: Tue, 26 Aug 2025 14:46:45 -0700 Subject: [PATCH 39/82] feat: add and enable reasoning and tool parser flags for trtllm and sglang (#2713) Signed-off-by: Jason Zhou --- .../backends/sglang/src/dynamo/sglang/args.py | 22 +++++++++++++++++- .../backends/sglang/src/dynamo/sglang/main.py | 5 +++- .../sglang/src/dynamo/sglang/register.py | 21 ++++++++++------- .../backends/trtllm/src/dynamo/trtllm/main.py | 12 ++++++++++ .../src/dynamo/trtllm/utils/trtllm_utils.py | 23 +++++++++++++++++++ 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/components/backends/sglang/src/dynamo/sglang/args.py b/components/backends/sglang/src/dynamo/sglang/args.py index a62f8f71f9..20df6e53a8 100644 --- a/components/backends/sglang/src/dynamo/sglang/args.py +++ b/components/backends/sglang/src/dynamo/sglang/args.py @@ -10,7 +10,7 @@ from argparse import Namespace from dataclasses import dataclass from enum import Enum -from typing import Any, Dict +from typing import Any, Dict, Optional from sglang.srt.server_args import ServerArgs @@ -39,6 +39,10 @@ class DynamoArgs: endpoint: str migration_limit: int + # tool and reasoning parser options + tool_call_parser: Optional[str] = None + reasoning_parser: Optional[str] = None + class DisaggregationMode(Enum): AGGREGATED = "agg" @@ -71,6 +75,20 @@ def parse_args(args: list[str]) -> Config: "--version", action="version", version=f"Dynamo Backend SGLang {__version__}" ) + # To avoid name conflicts with different backends, adoped prefix "dyn-" for dynamo specific args + parser.add_argument( + "--dyn-tool-call-parser", + type=str, + default=None, + help="Tool call parser name for the model. Available options: 'hermes', 'nemotron_deci', 'llama3_json', 'mistral', 'phi4'.", + ) + parser.add_argument( + "--dyn-reasoning-parser", + type=str, + default=None, + help="Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss'.", + ) + # Dynamo args for info in DYNAMO_ARGS.values(): parser.add_argument( @@ -123,6 +141,8 @@ def parse_args(args: list[str]) -> Config: component=parsed_component_name, endpoint=parsed_endpoint_name, migration_limit=parsed_args.migration_limit, + tool_call_parser=parsed_args.dyn_tool_call_parser, + reasoning_parser=parsed_args.dyn_reasoning_parser, ) logging.debug(f"Dynamo args: {dynamo_args}") diff --git a/components/backends/sglang/src/dynamo/sglang/main.py b/components/backends/sglang/src/dynamo/sglang/main.py index 604320b43c..a56f27fee0 100644 --- a/components/backends/sglang/src/dynamo/sglang/main.py +++ b/components/backends/sglang/src/dynamo/sglang/main.py @@ -97,7 +97,10 @@ async def gated_generate(request): async def register_model(): """Register the model and signal readiness""" registration_success = await register_llm_with_runtime_config( - engine, generate_endpoint, server_args, dynamo_args.migration_limit + engine, + generate_endpoint, + server_args, + dynamo_args, ) if not registration_success: diff --git a/components/backends/sglang/src/dynamo/sglang/register.py b/components/backends/sglang/src/dynamo/sglang/register.py index cd7a24d761..c2429f4eb3 100644 --- a/components/backends/sglang/src/dynamo/sglang/register.py +++ b/components/backends/sglang/src/dynamo/sglang/register.py @@ -9,20 +9,21 @@ from dynamo._core import Endpoint from dynamo.llm import ModelRuntimeConfig, ModelType, register_llm +from dynamo.sglang.args import DynamoArgs async def register_llm_with_runtime_config( engine: sgl.Engine, endpoint: Endpoint, server_args: ServerArgs, - migration_limit: int, + dynamo_args: DynamoArgs, ) -> bool: """Register LLM with runtime config Returns: bool: True if registration succeeded, False if it failed """ - runtime_config = await _get_runtime_config(engine) + runtime_config = await _get_runtime_config(engine, dynamo_args) try: await register_llm( ModelType.Backend, @@ -30,7 +31,7 @@ async def register_llm_with_runtime_config( server_args.model_path, server_args.served_model_name, kv_cache_block_size=server_args.page_size, - migration_limit=migration_limit, + migration_limit=dynamo_args.migration_limit, runtime_config=runtime_config, ) logging.info("Successfully registered LLM with runtime config") @@ -40,13 +41,17 @@ async def register_llm_with_runtime_config( return False -async def _get_runtime_config(engine: sgl.Engine) -> Optional[ModelRuntimeConfig]: +async def _get_runtime_config( + engine: sgl.Engine, dynamo_args: DynamoArgs +) -> Optional[ModelRuntimeConfig]: """Get runtime config from SGLang engine""" + runtime_config = ModelRuntimeConfig() + # set reasoning parser and tool call parser + runtime_config.reasoning_parser = dynamo_args.reasoning_parser + runtime_config.tool_call_parser = dynamo_args.tool_call_parser try: # Try to check if the engine has a scheduler attribute with the computed values if hasattr(engine, "scheduler_info") and engine.scheduler_info is not None: - runtime_config = ModelRuntimeConfig() - # Get max_total_num_tokens from scheduler_info if "max_total_num_tokens" in engine.scheduler_info: max_total_tokens = engine.scheduler_info["max_total_num_tokens"] @@ -73,8 +78,8 @@ async def _get_runtime_config(engine: sgl.Engine) -> Optional[ModelRuntimeConfig "The engine may compute these values internally after initialization. " "Proceeding without runtime config - SGLang will use its internal defaults." ) - return None + return runtime_config except Exception as e: logging.warning(f"Failed to get runtime config: {e}. Proceeding without it.") - return None + return runtime_config diff --git a/components/backends/trtllm/src/dynamo/trtllm/main.py b/components/backends/trtllm/src/dynamo/trtllm/main.py index 74934f1155..6bf5e63137 100644 --- a/components/backends/trtllm/src/dynamo/trtllm/main.py +++ b/components/backends/trtllm/src/dynamo/trtllm/main.py @@ -228,6 +228,17 @@ async def init(runtime: DistributedRuntime, config: Config): async with get_llm_engine(engine_args) as engine: endpoint = component.endpoint(config.endpoint) + # should ideally call get_engine_runtime_config + # this is because we don't have a good way to + # get total_kv_blocks from the engine yet without calling get_stats_async + # This causes an issue because get_stats_async doesn't work when no requests are sent to the engine + # So for now, we just set the parsers from the config + # TODO: fix this once we have a better way to get total_kv_blocks + runtime_config = ModelRuntimeConfig() + + runtime_config.reasoning_parser = config.reasoning_parser + runtime_config.tool_call_parser = config.tool_call_parser + if is_first_worker(config): # Register the model with runtime config await register_llm( @@ -237,6 +248,7 @@ async def init(runtime: DistributedRuntime, config: Config): config.served_model_name, kv_cache_block_size=config.kv_block_size, migration_limit=config.migration_limit, + runtime_config=runtime_config, ) # publisher will be set later if publishing is enabled. handler_config = RequestHandlerConfig( diff --git a/components/backends/trtllm/src/dynamo/trtllm/utils/trtllm_utils.py b/components/backends/trtllm/src/dynamo/trtllm/utils/trtllm_utils.py index 8c9e37538b..a23b4eb769 100644 --- a/components/backends/trtllm/src/dynamo/trtllm/utils/trtllm_utils.py +++ b/components/backends/trtllm/src/dynamo/trtllm/utils/trtllm_utils.py @@ -49,6 +49,9 @@ def __init__(self) -> None: self.next_endpoint: str = "" self.modality: str = "text" + self.reasoning_parser: Optional[str] = None + self.tool_call_parser: Optional[str] = None + def __str__(self) -> str: return ( f"Config(namespace={self.namespace}, " @@ -73,6 +76,8 @@ def __str__(self) -> str: f"disaggregation_strategy={self.disaggregation_strategy}, " f"next_endpoint={self.next_endpoint}, " f"modality={self.modality})" + f"reasoning_parser={self.reasoning_parser})" + f"tool_call_parser={self.tool_call_parser})" ) @@ -234,6 +239,21 @@ def cmd_line_args(): default="", help=f"Endpoint(in 'dyn://namespace.component.endpoint' format) to send requests to when running in disaggregation mode. Default: {DEFAULT_NEXT_ENDPOINT} if first worker, empty if next worker", ) + + # To avoid name conflicts with different backends, adoped prefix "dyn-" for dynamo specific args + parser.add_argument( + "--dyn-tool-call-parser", + type=str, + default=None, + help="Tool call parser name for the model. Available options: 'hermes', 'nemotron_deci', 'llama3_json', 'mistral', 'phi4'.", + ) + parser.add_argument( + "--dyn-reasoning-parser", + type=str, + default=None, + help="Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss'.", + ) + args = parser.parse_args() config = Config() @@ -294,4 +314,7 @@ def cmd_line_args(): config.publish_events_and_metrics = args.publish_events_and_metrics config.modality = args.modality + config.reasoning_parser = args.dyn_reasoning_parser + config.tool_call_parser = args.dyn_tool_call_parser + return config From e9fa569ed7b7b6c04b42ac888a54b8a780d99a87 Mon Sep 17 00:00:00 2001 From: mohammedabdulwahhab Date: Tue, 26 Aug 2025 15:33:06 -0700 Subject: [PATCH 40/82] fix: fix hello world (#2727) Signed-off-by: Jason Zhou --- examples/runtime/hello_world/deploy/hello_world.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/runtime/hello_world/deploy/hello_world.yaml b/examples/runtime/hello_world/deploy/hello_world.yaml index 1bf010cfc6..bf07c868bd 100644 --- a/examples/runtime/hello_world/deploy/hello_world.yaml +++ b/examples/runtime/hello_world/deploy/hello_world.yaml @@ -10,9 +10,11 @@ spec: services: Frontend: livenessProbe: - httpGet: - path: /health - port: 8000 + exec: + command: + - /bin/sh + - -c + - 'echo ok' initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 2 From e6f4121673edc41540a1308a47aa3716a99fcb3f Mon Sep 17 00:00:00 2001 From: atchernych Date: Tue, 26 Aug 2025 16:19:25 -0700 Subject: [PATCH 41/82] feat: Deployment for Dynamo EPP - aware gateway (#2633) Signed-off-by: atchernych Co-authored-by: hhzhang16 <54051230+hhzhang16@users.noreply.github.com> Signed-off-by: Jason Zhou --- deploy/inference-gateway/README.md | 84 +- .../v0.5.1-1/epp-v0.5.1-dyn1.patch | 794 ++++++++++++++++++ .../helm/dynamo-gaie/epp-config-dynamo.yaml | 38 + .../dynamo-gaie/templates/dynamo-epp.yaml | 23 +- .../dynamo-gaie/templates/epp-configmap.yaml | 25 + .../helm/dynamo-gaie/values.yaml | 2 +- .../inference-gateway/values-epp-aware.yaml | 2 +- 7 files changed, 938 insertions(+), 30 deletions(-) create mode 100644 deploy/inference-gateway/epp-patches/v0.5.1-1/epp-v0.5.1-dyn1.patch create mode 100644 deploy/inference-gateway/helm/dynamo-gaie/epp-config-dynamo.yaml create mode 100644 deploy/inference-gateway/helm/dynamo-gaie/templates/epp-configmap.yaml diff --git a/deploy/inference-gateway/README.md b/deploy/inference-gateway/README.md index ada2af2293..ac8ee42ea1 100644 --- a/deploy/inference-gateway/README.md +++ b/deploy/inference-gateway/README.md @@ -1,8 +1,12 @@ ## Inference Gateway Setup with Dynamo This guide demonstrates two setups. -The EPP-unaware setup treats each Dynamo deployment as a black box and routes traffic randomly among the deployments. -The EPP-aware setup first uses Dynamo Router to pick the worker instance id for serving the model. Then traffic gets directed straight to the selected worker. + +- The basic setup treats each Dynamo deployment as a black box and routes traffic randomly among the deployments. +- The EPP-aware setup uses a custom Dynamo plugin `dyn-kv` to pick the best worker. + +EPP’s default approach is token-aware only `by approximation` because it relies on the non-tokenized text in the prompt. But the Dynamo plugin uses a token-aware KV algorithm. It employs the dynamo router which implements kv routing by running your model’s tokenizer inline. The EPP plugin configuration lives in [`helm/dynamo-gaie/epp-config-dynamo.yaml`](helm/dynamo-gaie/epp-config-dynamo.yaml) per EPP [convention](https://gateway-api-inference-extension.sigs.k8s.io/guides/epp-configuration/config-text/). + Currently, these setups are only supported with the kGateway based Inference Gateway. ## Table of Contents @@ -18,12 +22,12 @@ Currently, these setups are only supported with the kGateway based Inference Gat ## Installation Steps -1. **Install Dynamo Platform** +### 1. Install Dynamo Platform ### [See Quickstart Guide](../../docs/guides/dynamo_deploy/README.md) to install Dynamo Cloud. -2. **Deploy Inference Gateway** +### 2. Deploy Inference Gateway ### First, deploy an inference gateway service. In this example, we'll install `kgateway` based gateway implementation. You can use the script below or follow the steps manually. @@ -72,7 +76,7 @@ kubectl get gateway inference-gateway -n my-model # inference-gateway kgateway x.x.x.x True 1m ``` -3. **Deploy model** +### 3. Deploy Your Model ### Follow the steps in [model deployment](../../components/backends/vllm/deploy/README.md) to deploy `Qwen/Qwen3-0.6B` model in aggregate mode using [agg.yaml](../../components/backends/vllm/deploy/agg.yaml) in `my-model` kubernetes namespace. @@ -81,51 +85,85 @@ Sample commands to deploy model: cd /components/backends/vllm/deploy kubectl apply -f agg.yaml -n my-model ``` +Take a note of or change the DYNAMO_IMAGE in the model deployment file. -4. **Install Dynamo GAIE helm chart** +### 4. Install Dynamo GAIE helm chart ### The Inference Gateway is configured through the `inference-gateway-resources.yaml` file. Deploy the Inference Gateway resources to your Kubernetes cluster by running one of the commands below. -For the EPP-unaware black box integration run: +#### Basic Black Box Integration #### + +For the basic black box integration run: ```bash cd deploy/inference-gateway helm install dynamo-gaie ./helm/dynamo-gaie -n my-model -f ./vllm_agg_qwen.yaml ``` -For the EPP-aware integration run: +#### EPP-aware Integration with the custom Dynamo Plugin #### + +##### 1. Build the custom EPP image ##### + +We provide git patches for you to use. + +##### 1.1 Clone the official GAIE repo in a separate folder ##### ```bash -cd deploy/inference-gateway +git clone https://github.com/kubernetes-sigs/gateway-api-inference-extension.git +cd gateway-api-inference-extension +git checkout v0.5.1 +``` -helm install dynamo-gaie ./helm/dynamo-gaie \ - -n my-model \ - -f ./vllm_agg_qwen.yaml \ - -f ./values-epp-aware.yaml +##### 1.2 Apply patch(es) ##### + +```bash +git apply /deploy/inference-gateway/epp-patches/v0.5.1-1/epp-v0.5.1-dyn1.patch +``` + +##### 1.3 Build the custom EPP image ##### + +```bash +# Build the image and then manually push +make image-local-load \ + IMAGE_REGISTRY= \ + IMAGE_NAME=dynamo-custom-epp \ + EXTRA_TAG= + +# Or run the command below to build push to your registry +make image-local-push \ + IMAGE_REGISTRY= \ + IMAGE_NAME=dynamo-custom-epp \ + EXTRA_TAG= ``` -Or customize the EPP further using flags, i.e: +##### 2. Install through helm ##### ```bash -helm install dynamo-gaie ./helm/dynamo-gaie \ +cd deploy/inference-gateway + +# Export the Dynamo image you have used when deploying your model in Step 3. +export DYNAMO_IMAGE= +export EPP_IMAGE= # i.e. docker.io/lambda108/epp-inference-extension-dynamo:v0.5.1-1 + +helm upgrade --install dynamo-gaie ./helm/dynamo-gaie \ -n my-model \ -f ./vllm_agg_qwen.yaml \ + -f ./values-epp-aware.yaml \ --set eppAware.enabled=true \ - --set eppAware.eppImage=docker.io/lambda108/epp-inference-extension-dynamo:1.0.0 \ - --set imagePullSecrets='{docker-imagepullsecret}' \ - --set-string epp.extraEnv[0].name=USE_STREAMING \ - --set-string epp.extraEnv[0].value=true + --set-string eppAware.eppImage=$EPP_IMAGE \ + --set-string eppAware.sidecar.image=$DYNAMO_IMAGE ``` + Key configurations include: - An InferenceModel resource for the Qwen model - A service for the inference gateway - Required RBAC roles and bindings - RBAC permissions -5. **Verify Installation** +### 5. Verify Installation ### Check that all resources are properly deployed: @@ -153,11 +191,11 @@ NAME HOSTNAMES AGE qwen-route 33m ``` -## Usage +### 6. Usage ### The Inference Gateway provides HTTP endpoints for model inference. -### 1: Populate gateway URL for your k8s cluster +#### 1: Populate gateway URL for your k8s cluster #### ```bash export GATEWAY_URL= ``` @@ -183,7 +221,7 @@ kubectl port-forward svc/inference-gateway 8000:80 -n my-model GATEWAY_URL=http://localhost:8000 ``` -### 2: Check models deployed to inference gateway +#### 2: Check models deployed to inference gateway #### a. Query models: diff --git a/deploy/inference-gateway/epp-patches/v0.5.1-1/epp-v0.5.1-dyn1.patch b/deploy/inference-gateway/epp-patches/v0.5.1-1/epp-v0.5.1-dyn1.patch new file mode 100644 index 0000000000..596ac8bec9 --- /dev/null +++ b/deploy/inference-gateway/epp-patches/v0.5.1-1/epp-v0.5.1-dyn1.patch @@ -0,0 +1,794 @@ +diff --git a/cmd/epp/main.go b/cmd/epp/main.go +index b5e0617..8592735 100644 +--- a/cmd/epp/main.go ++++ b/cmd/epp/main.go +@@ -22,6 +22,11 @@ import ( + ctrl "sigs.k8s.io/controller-runtime" + + "sigs.k8s.io/gateway-api-inference-extension/cmd/epp/runner" ++ eppplugins "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins" ++ ++ // Dynamo plugins ++ dynprereq "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol/plugins/dynamo_inject_workerid" ++ dynscorer "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/plugins/dynamo_kv_scorer" + ) + + func main() { +@@ -30,6 +35,9 @@ func main() { + // For adding out-of-tree plugins to the plugins registry, use the following: + // plugins.Register(my-out-of-tree-plugin-name, my-out-of-tree-plugin-factory-function) + ++ eppplugins.Register("dynamo-inject-workerid", dynprereq.InjectWorkerIDPreRequestFactory) ++ eppplugins.Register("kv-aware-scorer", dynscorer.KVAwareScorerFactory) ++ + if err := runner.NewRunner().Run(ctrl.SetupSignalHandler()); err != nil { + os.Exit(1) + } +diff --git a/pkg/bbr/handlers/request.go b/pkg/bbr/handlers/request.go +index 32fffc0..1aa1b85 100644 +--- a/pkg/bbr/handlers/request.go ++++ b/pkg/bbr/handlers/request.go +@@ -18,8 +18,10 @@ package handlers + + import ( + "context" ++ "encoding/base64" + "encoding/json" + "fmt" ++ "strings" + + basepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + eppb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" +@@ -31,11 +33,49 @@ import ( + + const modelHeader = "X-Gateway-Model-Name" + ++// Dynamo-related ++const ( ++ workerIDHeader = "x-worker-instance-id" ++ injectHintHeader = "x-epp-inject-nvext-worker-instance-id" ++ tokenDataHeader = "x-epp-inject-nvext-token-data" ++) ++ + // HandleRequestBody handles request bodies. + func (s *Server) HandleRequestBody(ctx context.Context, data map[string]any) ([]*eppb.ProcessingResponse, error) { + logger := log.FromContext(ctx) + var ret []*eppb.ProcessingResponse + ++ // If we captured a worker id hint in the headers phase, inject it into body JSON: ++ // nvext.backend_instance_id = ++ if wid := strings.TrimSpace(s.workerIDHint); wid != "" { ++ // ensure nvext is a map[string]any ++ if nv, ok := data["nvext"]; !ok || nv == nil { ++ data["nvext"] = map[string]any{"backend_instance_id": wid} ++ } else if m, ok := nv.(map[string]any); ok { ++ m["backend_instance_id"] = wid ++ } else { ++ // if nvext was some other type, replace with a clean map ++ data["nvext"] = map[string]any{"backend_instance_id": wid} ++ } ++ } ++ ++ // If we captured token_data in headers, decode and inject as nvext.token_data ++ if td := strings.TrimSpace(s.tokenDataHint); td != "" { ++ // header value is base64(JSON array) ++ if raw, err := base64.StdEncoding.DecodeString(td); err == nil { ++ var arr []int64 ++ if err := json.Unmarshal(raw, &arr); err == nil && len(arr) > 0 { ++ // ensure nvext map exists ++ nv, ok := data["nvext"].(map[string]any) ++ if !ok || nv == nil { ++ nv = map[string]any{} ++ data["nvext"] = nv ++ } ++ nv["token_data"] = arr ++ } ++ } ++ } ++ + requestBodyBytes, err := json.Marshal(data) + if err != nil { + return nil, err +@@ -46,6 +86,7 @@ func (s *Server) HandleRequestBody(ctx context.Context, data map[string]any) ([] + metrics.RecordModelNotInBodyCounter() + logger.V(logutil.DEFAULT).Info("Request body does not contain model parameter") + if s.streaming { ++ // still stream the possibly mutated body + ret = append(ret, &eppb.ProcessingResponse{ + Response: &eppb.ProcessingResponse_RequestHeaders{ + RequestHeaders: &eppb.HeadersResponse{}, +@@ -53,14 +94,24 @@ func (s *Server) HandleRequestBody(ctx context.Context, data map[string]any) ([] + }) + ret = addStreamedBodyResponse(ret, requestBodyBytes) + return ret, nil +- } else { +- ret = append(ret, &eppb.ProcessingResponse{ ++ } ++ ++ // non-streaming: return a body response with the (possibly) mutated body ++ return []*eppb.ProcessingResponse{ ++ { + Response: &eppb.ProcessingResponse_RequestBody{ +- RequestBody: &eppb.BodyResponse{}, ++ RequestBody: &eppb.BodyResponse{ ++ Response: &eppb.CommonResponse{ ++ BodyMutation: &eppb.BodyMutation{ ++ Mutation: &eppb.BodyMutation_Body{ ++ Body: requestBodyBytes, ++ }, ++ }, ++ }, ++ }, + }, +- }) +- } +- return ret, nil ++ }, ++ }, nil + } + + modelStr, ok := modelVal.(string) +@@ -73,6 +124,7 @@ func (s *Server) HandleRequestBody(ctx context.Context, data map[string]any) ([] + metrics.RecordSuccessCounter() + + if s.streaming { ++ // set the model header, then stream the (possibly) mutated body + ret = append(ret, &eppb.ProcessingResponse{ + Response: &eppb.ProcessingResponse_RequestHeaders{ + RequestHeaders: &eppb.HeadersResponse{ +@@ -86,16 +138,42 @@ func (s *Server) HandleRequestBody(ctx context.Context, data map[string]any) ([] + RawValue: []byte(modelStr), + }, + }, ++ // also keep the worker id header if we have one ++ func() *basepb.HeaderValueOption { ++ if strings.TrimSpace(s.workerIDHint) == "" { ++ return nil ++ } ++ return &basepb.HeaderValueOption{ ++ Header: &basepb.HeaderValue{ ++ Key: workerIDHeader, ++ RawValue: []byte(s.workerIDHint), ++ }, ++ } ++ }(), + }, + }, + }, + }, + }, + }) ++ ++ // prune nil entries if worker id not present ++ hm := ret[len(ret)-1].GetRequestHeaders().GetResponse().GetHeaderMutation() ++ if hm != nil && hm.SetHeaders != nil { ++ out := hm.SetHeaders[:0] ++ for _, h := range hm.SetHeaders { ++ if h != nil { ++ out = append(out, h) ++ } ++ } ++ hm.SetHeaders = out ++ } ++ + ret = addStreamedBodyResponse(ret, requestBodyBytes) + return ret, nil + } + ++ // Non-streaming: set model header and replace the body with our mutated JSON + return []*eppb.ProcessingResponse{ + { + Response: &eppb.ProcessingResponse_RequestBody{ +@@ -111,6 +189,22 @@ func (s *Server) HandleRequestBody(ctx context.Context, data map[string]any) ([] + RawValue: []byte(modelStr), + }, + }, ++ func() *basepb.HeaderValueOption { ++ if strings.TrimSpace(s.workerIDHint) == "" { ++ return nil ++ } ++ return &basepb.HeaderValueOption{ ++ Header: &basepb.HeaderValue{ ++ Key: workerIDHeader, ++ RawValue: []byte(s.workerIDHint), ++ }, ++ } ++ }(), ++ }, ++ }, ++ BodyMutation: &eppb.BodyMutation{ ++ Mutation: &eppb.BodyMutation_Body{ ++ Body: requestBodyBytes, + }, + }, + }, +@@ -141,6 +235,32 @@ func addStreamedBodyResponse(responses []*eppb.ProcessingResponse, requestBodyBy + + // HandleRequestHeaders handles request headers. + func (s *Server) HandleRequestHeaders(headers *eppb.HttpHeaders) ([]*eppb.ProcessingResponse, error) { ++ // reset per-request ++ s.workerIDHint = "" ++ s.tokenDataHint = "" ++ ++ if m := headers.GetHeaders(); m != nil { ++ for _, h := range m.GetHeaders() { ++ k := strings.ToLower(h.GetKey()) ++ ++ switch k { ++ case injectHintHeader, workerIDHeader: ++ if rv := h.GetRawValue(); len(rv) > 0 { ++ s.workerIDHint = strings.TrimSpace(string(rv)) ++ } else { ++ s.workerIDHint = strings.TrimSpace(h.GetValue()) ++ } ++ case tokenDataHeader: ++ if rv := h.GetRawValue(); len(rv) > 0 { ++ s.tokenDataHint = strings.TrimSpace(string(rv)) ++ } else { ++ s.tokenDataHint = strings.TrimSpace(h.GetValue()) ++ } ++ } ++ } ++ } ++ ++ // No header mutations needed here; body phase will do the JSON injection. + return []*eppb.ProcessingResponse{ + { + Response: &eppb.ProcessingResponse_RequestHeaders{ +diff --git a/pkg/bbr/handlers/server.go b/pkg/bbr/handlers/server.go +index a580380..eb2893f 100644 +--- a/pkg/bbr/handlers/server.go ++++ b/pkg/bbr/handlers/server.go +@@ -38,7 +38,9 @@ func NewServer(streaming bool) *Server { + // Server implements the Envoy external processing server. + // https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ext_proc/v3/external_processor.proto + type Server struct { +- streaming bool ++ streaming bool ++ workerIDHint string ++ tokenDataHint string + } + + func (s *Server) Process(srv extProcPb.ExternalProcessor_ProcessServer) error { +diff --git a/pkg/epp/requestcontrol/plugins/dynamo_inject_workerid/plugin.go b/pkg/epp/requestcontrol/plugins/dynamo_inject_workerid/plugin.go +new file mode 100644 +index 0000000..b6708fa +--- /dev/null ++++ b/pkg/epp/requestcontrol/plugins/dynamo_inject_workerid/plugin.go +@@ -0,0 +1,69 @@ ++package dynamo_inject_workerid ++ ++import ( ++ "context" ++ "encoding/json" ++ "strings" ++ ++ "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins" ++ rc "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol" ++ schedtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types" ++) ++ ++const ( ++ typeString = "dynamo-inject-workerid" ++ pluginName = "dynamo-inject-workerid" ++ WorkerIDHeader = "x-worker-instance-id" ++ injectHintHeader = "x-epp-inject-nvext-worker-instance-id" ++ TokenDataHeader = "x-epp-inject-nvext-token-data" ++) ++ ++var _ plugins.Plugin = (*InjectWorkerIDPreRequest)(nil) ++var _ rc.PreRequest = (*InjectWorkerIDPreRequest)(nil) ++ ++type InjectWorkerIDPreRequest struct { ++ typedName plugins.TypedName ++} ++ ++func NewInjectWorkerIDPreRequest() *InjectWorkerIDPreRequest { ++ return &InjectWorkerIDPreRequest{ ++ typedName: plugins.TypedName{Type: typeString, Name: pluginName}, ++ } ++} ++ ++func (p *InjectWorkerIDPreRequest) WithName(name string) *InjectWorkerIDPreRequest { ++ p.typedName.Name = name ++ return p ++} ++ ++func InjectWorkerIDPreRequestFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) { ++ return NewInjectWorkerIDPreRequest().WithName(name), nil ++} ++ ++func (p *InjectWorkerIDPreRequest) TypedName() plugins.TypedName { return p.typedName } ++ ++func (p *InjectWorkerIDPreRequest) PreRequest( ++ _ context.Context, ++ req *schedtypes.LLMRequest, ++ _ *schedtypes.SchedulingResult, ++ _ int, ++) { ++ if req == nil { ++ return ++ } ++ if req.Headers == nil { ++ req.Headers = map[string]string{} ++ } ++ wid := strings.TrimSpace(req.Headers[WorkerIDHeader]) ++ if wid == "" { ++ return ++ } ++ req.Headers[WorkerIDHeader] = wid ++ req.Headers[injectHintHeader] = wid ++ ++ // Pass through token-data header if scorer set it ++ if td := strings.TrimSpace(req.Headers[TokenDataHeader]); td != "" { ++ req.Headers[TokenDataHeader] = td ++ } ++ ++} +diff --git a/pkg/epp/scheduling/plugins/dynamo_kv_scorer/epp-config-dynamo.yaml b/pkg/epp/scheduling/plugins/dynamo_kv_scorer/epp-config-dynamo.yaml +new file mode 100644 +index 0000000..2d92be0 +--- /dev/null ++++ b/pkg/epp/scheduling/plugins/dynamo_kv_scorer/epp-config-dynamo.yaml +@@ -0,0 +1,24 @@ ++# This is an example for configuring the EPP to use the dynamo token-aware kv router for scoring the pods ++apiVersion: inference.networking.x-k8s.io/v1alpha1 ++kind: EndpointPickerConfig ++plugins: ++ # Required: tells EPP which profile to use (even if you only have one) ++ - type: single-profile-handler ++ ++ # Picker: chooses the final endpoint after scoring ++ - name: picker ++ type: max-score-picker ++ - name: dyn-pre ++ type: dynamo-inject-workerid ++ parameters: {} ++ - name: dyn-kv ++ type: kv-aware-scorer ++ parameters: ++ frontendURL: http://127.0.0.1:8000/v1/chat/completions ++ timeoutMS: 10000 ++schedulingProfiles: ++ - name: default ++ plugins: ++ - pluginRef: dyn-kv ++ weight: 1 ++ - pluginRef: picker +diff --git a/pkg/epp/scheduling/plugins/dynamo_kv_scorer/plugin.go b/pkg/epp/scheduling/plugins/dynamo_kv_scorer/plugin.go +new file mode 100644 +index 0000000..50eb5f6 +--- /dev/null ++++ b/pkg/epp/scheduling/plugins/dynamo_kv_scorer/plugin.go +@@ -0,0 +1,431 @@ ++package dynamo_kv_scorer ++ ++import ( ++ "bufio" ++ "bytes" ++ "context" ++ "encoding/base64" ++ "encoding/json" ++ "fmt" ++ "io" ++ "net/http" ++ "strings" ++ "time" ++ ++ log "sigs.k8s.io/controller-runtime/pkg/log" ++ "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins" ++ "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework" ++ schedtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types" ++ logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging" ++) ++ ++const ( ++ PluginName = "dynamo-kv-scorer" ++ KVAwareScorerType = "kv-aware-scorer" ++ StateKeyWorkerInstanceID = schedtypes.StateKey("dynamo/worker-instance-id") ++ WorkerIDHeader = "x-worker-instance-id" ++ TokenDataHeader = "x-epp-inject-nvext-token-data" ++) ++ ++type params struct { ++ FrontendURL string `json:"frontendURL"` ++ TimeoutMS int `json:"timeoutMS"` ++} ++ ++// tiny wrapper so we can store a string in CycleState ++type stateString string ++ ++func (s stateString) Clone() schedtypes.StateData { return s } ++ ++type KVAwareScorer struct { ++ typedName plugins.TypedName ++ feURL string ++ feTimeout time.Duration ++} ++ ++// compile-time assertions ++var _ plugins.Plugin = (*KVAwareScorer)(nil) ++var _ framework.Scorer = (*KVAwareScorer)(nil) ++ ++func NewKVAwareScorer() *KVAwareScorer { ++ return &KVAwareScorer{ ++ typedName: plugins.TypedName{Type: KVAwareScorerType, Name: PluginName}, ++ feURL: "http://127.0.0.1:8000/v1/chat/completions", ++ feTimeout: 10 * time.Second, ++ } ++} ++ ++func (k *KVAwareScorer) WithName(name string) *KVAwareScorer { k.typedName.Name = name; return k } ++func (k *KVAwareScorer) WithFrontend(url string, timeout time.Duration) *KVAwareScorer { ++ if url != "" { ++ k.feURL = url ++ } ++ if timeout > 0 { ++ k.feTimeout = timeout ++ } ++ return k ++} ++ ++func KVAwareScorerFactory(name string, raw json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) { ++ p := params{} ++ _ = json.Unmarshal(raw, &p) ++ timeout := time.Duration(p.TimeoutMS) * time.Millisecond ++ if timeout <= 0 { ++ timeout = 10 * time.Second ++ } ++ return NewKVAwareScorer().WithName(name).WithFrontend(p.FrontendURL, timeout), nil ++} ++ ++func (k *KVAwareScorer) TypedName() plugins.TypedName { return k.typedName } ++ ++func (k *KVAwareScorer) Score( ++ ctx context.Context, ++ cycle *schedtypes.CycleState, ++ req *schedtypes.LLMRequest, ++ pods []schedtypes.Pod, ++) map[schedtypes.Pod]float64 { ++ logger := log.FromContext(ctx) ++ ++ workerID, tokenData, err := k.callFrontEndForWorker(ctx, req) ++ if err != nil { ++ logger.V(logutil.DEFAULT).Error(err, "FrontEnd call failed; proceeding without worker id") ++ } else if workerID != "" { ++ cycle.Write(StateKeyWorkerInstanceID, stateString(workerID)) ++ if req.Headers == nil { ++ req.Headers = map[string]string{} ++ } ++ req.Headers[WorkerIDHeader] = workerID ++ if len(tokenData) > 0 { ++ if req.Headers == nil { ++ req.Headers = map[string]string{} ++ } ++ req.Headers[TokenDataHeader] = encodeTokenData(tokenData) ++ } ++ } ++ ++ // neutral/uniform scores – only your scorer runs in the profile, so this “wins” ++ out := make(map[schedtypes.Pod]float64, len(pods)) ++ for _, p := range pods { ++ out[p] = 1.0 ++ } ++ return out ++} ++ ++// Call the Dynamo FrontEnd and extract worker_instance_id via SSE. ++func (k *KVAwareScorer) callFrontEndForWorker( ++ ctx context.Context, ++ req *schedtypes.LLMRequest, ++) (string, []int64, error) { ++ logger := log.FromContext(ctx) ++ ++ feBody := buildFrontEndBodyFromLLMRequest(req) ++ payload, err := json.Marshal(feBody) ++ if err != nil { ++ logger.V(logutil.DEFAULT).Error(err, "Dynamo FrontEnd marshal failed") ++ return "", nil, fmt.Errorf("marshal FrontEnd body: %w", err) ++ } ++ ++ reqCtx, cancel := context.WithTimeout(ctx, k.feTimeout) ++ defer cancel() ++ ++ httpReq, err := http.NewRequestWithContext(reqCtx, http.MethodPost, k.feURL, bytes.NewReader(payload)) ++ if err != nil { ++ logger.V(logutil.DEFAULT).Error(err, "Dynamo FrontEnd request build failed") ++ return "", nil, fmt.Errorf("build FrontEnd request: %w", err) ++ } ++ httpReq.Header.Set("Content-Type", "application/json") ++ httpReq.Header.Set("Accept", "text/event-stream") ++ ++ client := &http.Client{Timeout: 0} ++ resp, err := client.Do(httpReq) ++ if err != nil { ++ logger.V(logutil.DEFAULT).Error(err, "Dynamo FrontEnd POST failed") ++ return "", nil, fmt.Errorf("FrontEnd POST failed: %w", err) ++ } ++ defer resp.Body.Close() ++ ++ if resp.StatusCode < 200 || resp.StatusCode >= 300 { ++ errBody, _ := io.ReadAll(resp.Body) ++ logger.V(logutil.DEFAULT).Error(nil, "Dynamo FrontEnd non-2xx response", ++ "status_code", resp.StatusCode, "response_body", string(errBody)) ++ return "", nil, fmt.Errorf("Dynamo FrontEnd error: %d body=%s", resp.StatusCode, string(errBody)) ++ } ++ ++ ct := strings.ToLower(resp.Header.Get("Content-Type")) ++ if !strings.Contains(ct, "text/event-stream") { ++ logger.V(logutil.DEFAULT).Error(nil, "Unexpected non-SSE response") ++ return "", nil, fmt.Errorf("unexpected non-SSE response (Content-Type=%q)", resp.Header.Get("Content-Type")) ++ } ++ ++ // Parse SSE: expect `event: worker_instance_id`, a quoted id in a comment or data, and `data: [DONE]` ++ reader := bufio.NewReader(resp.Body) ++ workerID, tokenData, perr := parseSelectionFromSSE(ctx, reader) ++ if perr != nil { ++ return "", nil, perr ++ } ++ return workerID, tokenData, nil ++} ++ ++// Build the exact body we send to the FrontEnd, only from LLMRequest (no header merging). ++func buildFrontEndBodyFromLLMRequest(req *schedtypes.LLMRequest) map[string]any { ++ feBody := make(map[string]any, 8) ++ ++ // We call /v1/chat/completions so must provide messages ++ userText := "" ++ if req != nil && strings.TrimSpace(req.Prompt) != "" { ++ userText = req.Prompt ++ } ++ feBody["messages"] = []map[string]any{ ++ {"role": "user", "content": userText}, ++ } ++ ++ if req != nil && strings.TrimSpace(req.TargetModel) != "" { ++ feBody["model"] = req.TargetModel ++ } ++ ++ // Force SSE so we can parse worker_instance_id ++ feBody["stream"] = true ++ ++ feBody["max_tokens"] = 1 ++ feBody["temperature"] = 0.0 ++ ++ // Ask the Dynamo to include worker id ++ feBody["nvext"] = map[string]any{ ++ "annotations": []string{"query_instance_id"}, ++ } ++ ++ return feBody ++} ++ ++// This function scans an SSE stream for a worker_instance_id and token_data. ++// Expected pattern: ++// ++// event: worker_instance_id ++// : "8303679623149182543" ++// data: [DONE] ++ ++// or with tokens: ++// event: worker_instance_id\n: \"8228244551594056720\"\n\n ++// event: token_data\n: \"[151644,872,198,151644,872,198,14990,151645,198,151645,198,151644,77091,198]\ ++// "\n\ndata: [DONE]\n\n" ++// Also supports JSON in data lines with either top-level worker_instance_id ++// or annotations.worker_instance_id. ++func parseSelectionFromSSE(ctx context.Context, reader *bufio.Reader) (string, []int64, error) { ++ logger := log.FromContext(ctx) ++ ++ var ( ++ eventName string ++ dataBuf strings.Builder // accumulates "data:" lines for one event ++ commentBuf strings.Builder // accumulates ":" comment lines ++ gotWID string ++ gotTD []int64 ++ ) ++ ++ // collect the exact SSE bytes for debugging ++ var rawBuf strings.Builder ++ ++ flushEvent := func() (bool, error) { ++ data := strings.TrimSpace(dataBuf.String()) ++ comment := strings.TrimSpace(commentBuf.String()) ++ dataBuf.Reset() ++ commentBuf.Reset() ++ ++ // [DONE] ends the stream ++ if data == "[DONE]" || comment == "[DONE]" { ++ logger.V(logutil.DEFAULT).Info("SSE stream DONE") ++ logger.V(logutil.DEFAULT).Info("SSE raw stream", "raw", rawBuf.String()) ++ if gotWID != "" && len(gotTD) == 0 { ++ logger.V(logutil.DEFAULT).Info("SSE DONE: worker_instance_id present, token_data missing") ++ } ++ return true, nil ++ } ++ ++ // Prefer the named event ++ if eventName == "worker_instance_id" { ++ candidate := data ++ if candidate == "" { ++ candidate = comment ++ } ++ if candidate != "" { ++ // Try JSON string ++ var s string ++ if json.Unmarshal([]byte(candidate), &s) == nil && s != "" { ++ logger.V(logutil.VERBOSE).Info("worker_instance_id extracted from named event", "worker_instance_id", s) ++ gotWID = s ++ return false, nil ++ } ++ // Fallback: strip quotes ++ clean := strings.Trim(candidate, "\"") ++ if clean != "" && clean != "[DONE]" { ++ logger.V(logutil.DEFAULT).Info("worker_instance_id extracted (raw) from named event", "worker_instance_id", clean) ++ gotWID = clean ++ return false, nil ++ } ++ } ++ } ++ ++ if eventName == "token_data" { ++ candidate := data ++ if candidate == "" { ++ candidate = comment ++ } ++ if candidate != "" { ++ if arr := toInt64SliceJSON(candidate); len(arr) > 0 { ++ gotTD = arr ++ logger.V(logutil.DEFAULT).Info("token_data extracted from named event", "count", len(arr)) ++ return false, nil ++ } ++ } ++ } ++ // Generic JSON in data: ++ if data != "" { ++ var msg map[string]any ++ if json.Unmarshal([]byte(data), &msg) == nil { ++ if wid, ok := msg["worker_instance_id"].(string); ok && wid != "" { ++ logger.V(logutil.DEFAULT).Info("worker_instance_id found in SSE payload root", "worker_instance_id", wid) ++ gotWID = wid ++ } ++ if ann, ok := msg["annotations"].(map[string]any); ok { ++ if wid, ok := ann["worker_instance_id"].(string); ok && wid != "" { ++ logger.V(logutil.DEFAULT).Info("worker_instance_id found in SSE annotations", "worker_instance_id", wid) ++ gotWID = wid ++ } ++ } ++ if td, ok := msg["token_data"]; ok { ++ if arr := toInt64Slice(td); len(arr) > 0 { ++ gotTD = arr ++ logger.V(logutil.DEFAULT).Info("token_data found in SSE payload root", "count", len(arr)) ++ } ++ } else if nv, ok := msg["nvext"].(map[string]any); ok { ++ if td, ok := nv["token_data"]; ok { ++ if arr := toInt64Slice(td); len(arr) > 0 { ++ gotTD = arr ++ logger.V(logutil.DEFAULT).Info("token_data found in SSE nvext", "count", len(arr)) ++ } ++ } ++ } ++ } ++ } ++ return false, nil ++ } ++ ++ for { ++ line, err := reader.ReadString('\n') ++ // capture the raw stream as-is for debugging ++ rawBuf.WriteString(line) ++ if err != nil { ++ if err == io.EOF { ++ _, _ = flushEvent() ++ logger.V(logutil.DEFAULT).Info("SSE raw stream (EOF)", "raw", rawBuf.String()) ++ if gotWID != "" && len(gotTD) == 0 { ++ logger.V(logutil.DEFAULT).Info("EOF: worker_instance_id present, token_data missing") ++ } ++ if gotWID != "" || len(gotTD) > 0 { ++ return gotWID, gotTD, nil ++ } ++ logger.V(logutil.DEFAULT).Error(nil, "EOF before selection fields present") ++ return "", nil, fmt.Errorf("selection not found in SSE stream (EOF)") ++ } ++ logger.V(logutil.DEFAULT).Error(err, "SSE read error") ++ return "", nil, fmt.Errorf("sse read error: %w", err) ++ } ++ ++ l := strings.TrimRight(line, "\r\n") ++ if l == "" { ++ // End of current event. ++ if done, _ := flushEvent(); done { ++ if gotWID != "" && len(gotTD) == 0 { ++ logger.V(logutil.DEFAULT).Info("SSE DONE: worker_instance_id present, token_data missing") ++ } ++ return gotWID, gotTD, nil ++ } ++ eventName = "" // reset for next event ++ continue ++ } ++ ++ // Comment line ++ if strings.HasPrefix(l, ":") { ++ commentLine := strings.TrimSpace(l[1:]) ++ if commentBuf.Len() > 0 { ++ commentBuf.WriteByte('\n') ++ } ++ commentBuf.WriteString(commentLine) ++ continue ++ } ++ ++ // "field: value" ++ if idx := strings.IndexByte(l, ':'); idx != -1 { ++ field := l[:idx] ++ val := strings.TrimSpace(l[idx+1:]) ++ switch field { ++ case "event": ++ eventName = val ++ case "data": ++ if dataBuf.Len() > 0 { ++ dataBuf.WriteByte('\n') ++ } ++ dataBuf.WriteString(val) ++ default: ++ // ignore id, retry, etc. ++ } ++ } ++ } ++} ++ ++// encodeTokenData turns []int64 into base64(JSON array) for a safe header value. ++func encodeTokenData(tokens []int64) string { ++ b, _ := json.Marshal(tokens) ++ return base64.StdEncoding.EncodeToString(b) ++} ++ ++// Accepts interface{} from a parsed JSON map ++func toInt64Slice(v any) []int64 { ++ xs, ok := v.([]any) ++ if !ok { ++ return nil ++ } ++ out := make([]int64, 0, len(xs)) ++ for _, it := range xs { ++ switch n := it.(type) { ++ case float64: ++ out = append(out, int64(n)) ++ case int64: ++ out = append(out, n) ++ case json.Number: ++ if i, err := n.Int64(); err == nil { ++ out = append(out, i) ++ } ++ } ++ } ++ return out ++} ++ ++// Accepts raw JSON (string) for events like: ++// event: worker_instance_id\n: \"8228244551594056720\"\n\n ++// event: token_data\n: \"[151644,872,198,151644,872,198,14990,151645,198,151645,198,151644,77091,198]\ ++// "\n\ndata: [DONE]\n\n" ++// replaces the old toInt64SliceJSON ++func toInt64SliceJSON(s string) []int64 { ++ // case 1: direct JSON array ++ var arr []int64 ++ if err := json.Unmarshal([]byte(s), &arr); err == nil && len(arr) > 0 { ++ return arr ++ } ++ // case 2: s is a JSON string that itself contains a JSON array ++ var inner string ++ if err := json.Unmarshal([]byte(s), &inner); err == nil && inner != "" { ++ var arr2 []int64 ++ if err := json.Unmarshal([]byte(inner), &arr2); err == nil && len(arr2) > 0 { ++ return arr2 ++ } ++ } ++ // case 3: strip quotes and try once more ++ unquoted := strings.Trim(s, "\"") ++ if unquoted != s { ++ var arr3 []int64 ++ if err := json.Unmarshal([]byte(unquoted), &arr3); err == nil && len(arr3) > 0 { ++ return arr3 ++ } ++ } ++ return nil ++} diff --git a/deploy/inference-gateway/helm/dynamo-gaie/epp-config-dynamo.yaml b/deploy/inference-gateway/helm/dynamo-gaie/epp-config-dynamo.yaml new file mode 100644 index 0000000000..cedc139f0b --- /dev/null +++ b/deploy/inference-gateway/helm/dynamo-gaie/epp-config-dynamo.yaml @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +apiVersion: inference.networking.x-k8s.io/v1alpha1 +kind: EndpointPickerConfig +plugins: + # Required: tells EPP which profile to use (even if you only have one) + - type: single-profile-handler + + # Picker: chooses the final endpoint after scoring + - name: picker + type: max-score-picker + - name: dyn-pre + type: dynamo-inject-workerid + parameters: {} + - name: dyn-kv + type: kv-aware-scorer + parameters: + frontendURL: http://127.0.0.1:8000/v1/chat/completions + timeoutMS: 10000 +schedulingProfiles: + - name: default + plugins: + - pluginRef: dyn-kv + weight: 1 + - pluginRef: picker diff --git a/deploy/inference-gateway/helm/dynamo-gaie/templates/dynamo-epp.yaml b/deploy/inference-gateway/helm/dynamo-gaie/templates/dynamo-epp.yaml index 5f4b4c4b53..37ed1986be 100644 --- a/deploy/inference-gateway/helm/dynamo-gaie/templates/dynamo-epp.yaml +++ b/deploy/inference-gateway/helm/dynamo-gaie/templates/dynamo-epp.yaml @@ -41,11 +41,7 @@ spec: containers: - name: epp - image: {{ if .Values.eppAware.enabled }} - {{ default .Values.extension.image .Values.eppAware.eppImage }} - {{ else }} - {{ .Values.extension.image }} - {{ end }} + image: {{ if .Values.eppAware.enabled }}{{ default .Values.extension.image .Values.eppAware.eppImage }}{{ else }}{{ .Values.extension.image }}{{ end }} imagePullPolicy: {{ .Values.epp.imagePullPolicy | default "IfNotPresent" }} args: {{- if .Values.epp.argsOverride }} @@ -63,6 +59,14 @@ spec: - "9002" - -grpcHealthPort - "9003" + - -configFile + - "/etc/epp/epp-config-dynamo.yaml" + {{- end }} + {{- if .Values.eppAware.enabled }} + volumeMounts: + - name: epp-config + mountPath: /etc/epp + readOnly: true {{- end }} env: {{- range .Values.epp.extraEnv }} @@ -107,4 +111,13 @@ spec: {{- toYaml .Values.eppAware.sidecar.ports | nindent 8 }} resources: {{- toYaml .Values.eppAware.sidecar.resources | nindent 10 }} + {{- end }} + {{- if .Values.eppAware.enabled }} + volumes: + - name: epp-config + configMap: + name: {{ include "dynamo-gaie.fullname" . }}-epp-config + items: + - key: epp-config-dynamo.yaml + path: epp-config-dynamo.yaml {{- end }} \ No newline at end of file diff --git a/deploy/inference-gateway/helm/dynamo-gaie/templates/epp-configmap.yaml b/deploy/inference-gateway/helm/dynamo-gaie/templates/epp-configmap.yaml new file mode 100644 index 0000000000..80dd71642d --- /dev/null +++ b/deploy/inference-gateway/helm/dynamo-gaie/templates/epp-configmap.yaml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "dynamo-gaie.fullname" . }}-epp-config + labels: + app.kubernetes.io/name: {{ include "dynamo-gaie.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +data: + epp-config-dynamo.yaml: | +{{ (.Files.Get "epp-config-dynamo.yaml") | indent 4 }} diff --git a/deploy/inference-gateway/helm/dynamo-gaie/values.yaml b/deploy/inference-gateway/helm/dynamo-gaie/values.yaml index 401b27cfb9..b34591f63c 100644 --- a/deploy/inference-gateway/helm/dynamo-gaie/values.yaml +++ b/deploy/inference-gateway/helm/dynamo-gaie/values.yaml @@ -66,7 +66,7 @@ epp: eppAware: enabled: false # Optional: override EPP image when epp-aware=true - eppImage: docker.io/lambda108/epp-inference-extension-dynamo:1.0.0 + eppImage: docker.io/lambda108/epp-inference-extension-dynamo:v0.5.1-1 # Sidecar (frontend-router) sidecar: diff --git a/deploy/inference-gateway/values-epp-aware.yaml b/deploy/inference-gateway/values-epp-aware.yaml index eba30d1c70..4ff3c58119 100644 --- a/deploy/inference-gateway/values-epp-aware.yaml +++ b/deploy/inference-gateway/values-epp-aware.yaml @@ -15,7 +15,7 @@ eppAware: enabled: true - eppImage: docker.io/lambda108/epp-inference-extension-dynamo:1.0.0 + eppImage: docker.io/lambda108/epp-inference-extension-dynamo:v0.5.1-1 imagePullSecrets: - docker-imagepullsecret From db69d05a5317ae67b62fc58082377664ccaabf03 Mon Sep 17 00:00:00 2001 From: mohammedabdulwahhab Date: Tue, 26 Aug 2025 17:16:27 -0700 Subject: [PATCH 42/82] fix: add label to persist DGD name on downstream pods (#2729) Signed-off-by: Jason Zhou --- .../cloud/operator/internal/consts/consts.go | 1 + .../dynamocomponentdeployment_controller.go | 3 + ...namocomponentdeployment_controller_test.go | 6 +- .../cloud/operator/internal/dynamo/graph.go | 2 + .../operator/internal/dynamo/graph_test.go | 186 ++++++++++-------- 5 files changed, 118 insertions(+), 80 deletions(-) diff --git a/deploy/cloud/operator/internal/consts/consts.go b/deploy/cloud/operator/internal/consts/consts.go index 97b0aeeb85..d76f0e4919 100644 --- a/deploy/cloud/operator/internal/consts/consts.go +++ b/deploy/cloud/operator/internal/consts/consts.go @@ -23,6 +23,7 @@ const ( KubeAnnotationEnableGrove = "nvidia.com/enable-grove" + KubeLabelDynamoGraphDeploymentName = "nvidia.com/dynamo-graph-deployment-name" KubeLabelDynamoComponent = "nvidia.com/dynamo-component" KubeLabelDynamoNamespace = "nvidia.com/dynamo-namespace" KubeLabelDynamoDeploymentTargetType = "nvidia.com/dynamo-deployment-target-type" diff --git a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go index d7ce9a2df9..455f5da6f4 100644 --- a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go +++ b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go @@ -1146,6 +1146,9 @@ func (r *DynamoComponentDeploymentReconciler) generatePodTemplateSpec(ctx contex podLabels[commonconsts.KubeLabelMetricsEnabled] = commonconsts.KubeLabelValueTrue } + // Add label for the dynamo graph deployment on the pods themselves + podLabels[commonconsts.KubeLabelDynamoGraphDeploymentName] = opt.dynamoComponentDeployment.Spec.Labels[commonconsts.KubeLabelDynamoGraphDeploymentName] + // Add component type label if specified if opt.dynamoComponentDeployment.Spec.ComponentType != "" { podLabels[commonconsts.KubeLabelDynamoComponentType] = opt.dynamoComponentDeployment.Spec.ComponentType diff --git a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go index 0a85077ab9..afcbb52279 100644 --- a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go +++ b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go @@ -782,7 +782,8 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, "role": "leader", "nvidia.com/label1": "label1", - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelDynamoGraphDeploymentName: "", }, Annotations: map[string]string{ "scheduling.k8s.io/group-name": "test-lws-deploy-0", @@ -891,7 +892,8 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing. commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, "role": "worker", "nvidia.com/label1": "label1", - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelDynamoGraphDeploymentName: "", }, Annotations: map[string]string{ "scheduling.k8s.io/group-name": "test-lws-deploy-0", diff --git a/deploy/cloud/operator/internal/dynamo/graph.go b/deploy/cloud/operator/internal/dynamo/graph.go index 9f3c596294..962e56e5f2 100644 --- a/deploy/cloud/operator/internal/dynamo/graph.go +++ b/deploy/cloud/operator/internal/dynamo/graph.go @@ -138,6 +138,7 @@ func GenerateDynamoComponentsDeployments(ctx context.Context, parentDynamoGraphD deployment.Labels = labels labels[commonconsts.KubeLabelDynamoComponent] = componentName labels[commonconsts.KubeLabelDynamoNamespace] = graphDynamoNamespace + labels[commonconsts.KubeLabelDynamoGraphDeploymentName] = parentDynamoGraphDeployment.Name // Propagate metrics annotation from parent deployment if present if parentDynamoGraphDeployment.Annotations != nil { @@ -960,6 +961,7 @@ func GenerateGrovePodGangSet( func generateLabels(component *v1alpha1.DynamoComponentDeploymentOverridesSpec, dynamoDeployment *v1alpha1.DynamoGraphDeployment, componentName string) (map[string]string, error) { labels := make(map[string]string) labels[commonconsts.KubeLabelDynamoSelector] = GetDynamoComponentName(dynamoDeployment, componentName) + labels[commonconsts.KubeLabelDynamoGraphDeploymentName] = dynamoDeployment.Name if component.ComponentType != "" { labels[commonconsts.KubeLabelDynamoComponentType] = component.ComponentType } diff --git a/deploy/cloud/operator/internal/dynamo/graph_test.go b/deploy/cloud/operator/internal/dynamo/graph_test.go index ba58d916d2..64437f0a28 100644 --- a/deploy/cloud/operator/internal/dynamo/graph_test.go +++ b/deploy/cloud/operator/internal/dynamo/graph_test.go @@ -99,8 +99,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service1", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -118,8 +119,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { }, }, Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Autoscaling: nil, }, @@ -130,8 +132,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service2", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -140,8 +143,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { DynamoNamespace: &[]string{"default"}[0], Replicas: &[]int32{3}[0], Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Resources: &common.Resources{ Requests: &common.ResourceItem{ @@ -207,8 +211,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service1", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -226,8 +231,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { }, }, Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Autoscaling: nil, }, @@ -238,8 +244,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service2", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -248,8 +255,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { DynamoNamespace: &[]string{"dynamo-test-dynamographdeployment"}[0], Replicas: &[]int32{3}[0], Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Resources: &common.Resources{ Requests: &common.ResourceItem{ @@ -365,8 +373,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service1", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -384,8 +393,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { }, }, Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Autoscaling: nil, Ingress: &v1alpha1.IngressSpec{ @@ -400,8 +410,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service2", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -410,8 +421,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { DynamoNamespace: &[]string{"dynamo-test-dynamographdeployment"}[0], Replicas: &[]int32{3}[0], Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Resources: &common.Resources{ Requests: &common.ResourceItem{ @@ -483,8 +495,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service1", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -508,8 +521,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { }, }, Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Autoscaling: nil, Envs: []corev1.EnvVar{ @@ -526,8 +540,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service2", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -536,8 +551,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { DynamoNamespace: &[]string{"dynamo-test-dynamographdeployment"}[0], Replicas: &[]int32{3}[0], Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Resources: &common.Resources{ Requests: &common.ResourceItem{ @@ -616,8 +632,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service1", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -636,8 +653,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { }, }, Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service1", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service1", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Autoscaling: nil, ExtraPodSpec: &common.ExtraPodSpec{ @@ -654,8 +672,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { Name: "test-dynamographdeployment-service2", Namespace: "default", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, }, Spec: v1alpha1.DynamoComponentDeploymentSpec{ @@ -665,8 +684,9 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) { DynamoNamespace: &[]string{"default"}[0], Replicas: &[]int32{3}[0], Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponent: "service2", - commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoComponent: "service2", + commonconsts.KubeLabelDynamoNamespace: "default", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamographdeployment", }, Resources: &common.Resources{ Requests: &common.ResourceItem{ @@ -1214,11 +1234,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "frontend", Labels: map[string]string{ - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-frontend", - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeFrontend, - "nvidia.com/label1": "label1", - "nvidia.com/label2": "label2", + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-frontend", + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeFrontend, + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", + "nvidia.com/label1": "label1", + "nvidia.com/label2": "label2", }, Annotations: map[string]string{ "nvidia.com/annotation1": "annotation1", @@ -1355,8 +1376,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "planner", Labels: map[string]string{ - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-planner", + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-planner", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", }, Annotations: map[string]string{}, Spec: grovev1alpha1.PodCliqueSpec{ @@ -1710,11 +1732,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "worker-ldr", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-ldr", - "nvidia.com/label1": "label1", - "nvidia.com/label2": "label2", + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-ldr", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", + "nvidia.com/label1": "label1", + "nvidia.com/label2": "label2", }, Annotations: map[string]string{ "nvidia.com/annotation1": "annotation1", @@ -1859,11 +1882,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "worker-wkr", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-wkr", - "nvidia.com/label1": "label1", - "nvidia.com/label2": "label2", + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-wkr", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", + "nvidia.com/label1": "label1", + "nvidia.com/label2": "label2", }, Annotations: map[string]string{ "nvidia.com/annotation1": "annotation1", @@ -1973,9 +1997,10 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "frontend", Labels: map[string]string{ - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-frontend", - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeFrontend, + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-frontend", + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeFrontend, + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", }, Annotations: map[string]string{}, Spec: grovev1alpha1.PodCliqueSpec{ @@ -2109,8 +2134,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "planner", Labels: map[string]string{ - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-planner", - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-planner", + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", }, Annotations: map[string]string{}, Spec: grovev1alpha1.PodCliqueSpec{ @@ -2488,11 +2514,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "worker-ldr", Labels: map[string]string{ - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-ldr", - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, - "nvidia.com/label1": "label1", - "nvidia.com/label2": "label2", + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-ldr", + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", + "nvidia.com/label1": "label1", + "nvidia.com/label2": "label2", }, Annotations: map[string]string{ "nvidia.com/annotation1": "annotation1", @@ -2625,11 +2652,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "worker-wkr", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-wkr", - "nvidia.com/label1": "label1", - "nvidia.com/label2": "label2", + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeWorker, + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-worker-wkr", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", + "nvidia.com/label1": "label1", + "nvidia.com/label2": "label2", }, Annotations: map[string]string{ "nvidia.com/annotation1": "annotation1", @@ -2739,9 +2767,10 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "frontend", Labels: map[string]string{ - commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeFrontend, - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-frontend", + commonconsts.KubeLabelDynamoComponentType: commonconsts.ComponentTypeFrontend, + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-frontend", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", }, Annotations: map[string]string{}, Spec: grovev1alpha1.PodCliqueSpec{ @@ -2875,8 +2904,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) { { Name: "planner", Labels: map[string]string{ - commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, - commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-planner", + commonconsts.KubeLabelMetricsEnabled: commonconsts.KubeLabelValueTrue, + commonconsts.KubeLabelDynamoSelector: "test-dynamo-graph-deployment-planner", + commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dynamo-graph-deployment", }, Annotations: map[string]string{}, Spec: grovev1alpha1.PodCliqueSpec{ @@ -4493,7 +4523,7 @@ func TestGenerateBasePodSpec_Worker(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: func() *resource.Quantity { q := resource.MustParse("512Mi"); return &q }(), + SizeLimit: func() *resource.Quantity { q := resource.MustParse("8Gi"); return &q }(), }, }, }, From 4931da171429339198e74ec6132dd617f43b8646 Mon Sep 17 00:00:00 2001 From: Dmitry Tokarev Date: Tue, 26 Aug 2025 22:37:03 -0400 Subject: [PATCH 43/82] fix: container/Dockerfile.trtllm - use pytorch 2.8.0a0+5228986c39.nv25.5 (#2579) Signed-off-by: Dmitry Tokarev Co-authored-by: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Signed-off-by: Jason Zhou --- container/Dockerfile.trtllm | 53 +++++++++++++++++++-------------- container/deps/requirements.txt | 2 +- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/container/Dockerfile.trtllm b/container/Dockerfile.trtllm index 91ce53cc80..9d846adfca 100644 --- a/container/Dockerfile.trtllm +++ b/container/Dockerfile.trtllm @@ -349,8 +349,6 @@ WORKDIR /workspace ARG ARCH_ALT ENV DYNAMO_HOME=/workspace -ENV VIRTUAL_ENV=/opt/dynamo/venv -ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" ENV NIXL_PREFIX=/opt/nvidia/nvda_nixl ENV NIXL_LIB_DIR=$NIXL_PREFIX/lib/${ARCH_ALT}-linux-gnu ENV NIXL_PLUGIN_DIR=$NIXL_LIB_DIR/plugins @@ -370,6 +368,7 @@ RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ python3-dev \ + python3-pip \ # jq and curl for polling various endpoints and health checks jq \ curl \ @@ -377,11 +376,14 @@ RUN apt-get update && \ vim \ # support UCX to establish connections with zmq libzmq3-dev \ + # install cudnn libs + libcudnn9-cuda-12 \ # Libraries required by UCX to find RDMA devices libibverbs1 rdma-core ibverbs-utils libibumad3 \ libnuma1 librdmacm1 ibverbs-providers \ openssh-client \ openssh-server && \ + ln -s /usr/bin/python3 /usr/bin/python && \ rm -rf /var/lib/apt/lists/* # Copy all bindings (wheels, lib, include) from dev image @@ -400,20 +402,18 @@ COPY --from=build /opt/hpcx/ompi /opt/hpcx/ompi # Copy NUMA library from build image COPY --from=build /usr/lib/${ARCH_ALT}-linux-gnu/libnuma.so* /usr/lib/${ARCH_ALT}-linux-gnu/ -# Setup the python environment -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -RUN uv venv $VIRTUAL_ENV --python 3.12 && \ - echo "source $VIRTUAL_ENV/bin/activate" >> ~/.bashrc # Common dependencies # TODO: Remove extra install and use pyproject.toml to define all dependencies RUN --mount=type=bind,source=./container/deps/requirements.txt,target=/tmp/requirements.txt \ - uv pip install --requirement /tmp/requirements.txt + python3 -m pip install --no-cache-dir --break-system-packages --requirement /tmp/requirements.txt && \ + echo "uninstall (networkx packaging torch triton) as we will use NVIDIA's versions later" && \ + python3 -m pip uninstall --yes --break-system-packages networkx packaging torch triton # Install test dependencies # TODO: Remove this once we have a functional CI image built on top of the runtime image RUN --mount=type=bind,source=./container/deps/requirements.test.txt,target=/tmp/requirements.txt \ - uv pip install --requirement /tmp/requirements.txt + python3 -m pip install --no-cache-dir --break-system-packages --requirement /tmp/requirements.txt # Copy CUDA toolkit components needed for nvcc, cudafe, cicc etc. COPY --from=build /usr/local/cuda/bin/nvcc /usr/local/cuda/bin/nvcc @@ -435,26 +435,28 @@ ARG SYMPY_VER=1.14.0 ARG PACKAGING_VER=23.2 ARG FLASH_ATTN_VER=2.7.4.post1 ARG MPMATH_VER=1.3.0 + COPY --from=build /usr/local/lib/lib* /usr/local/lib/ +COPY --from=build /usr/local/cuda-12.9/targets/x86_64-linux/lib/libcupti* /usr/local/cuda/targets/x86_64-linux/lib/ +# Copy UCX libraries, libucc.so is needed by pytorch. May not need to copy whole hpcx dir but only /opt/hpcx/ucc/ +COPY --from=build /opt/hpcx /opt/hpcx +# This is needed to make libucc.so visible so pytorch can use it. +ENV LD_LIBRARY_PATH="/opt/hpcx/ucc/lib:${LD_LIBRARY_PATH}" +# Might not need to copy cusparseLt in the future once it's included in DLFW cuda container +# networkx, packaging, setuptools get overridden by trtllm installation, so not copying them +# pytorch-triton is copied after trtllm installation. +COPY --from=build /usr/local/cuda/lib64/libcusparseLt* /usr/local/cuda/lib64/ COPY --from=build /usr/local/lib/python3.12/dist-packages/torch /usr/local/lib/python3.12/dist-packages/torch COPY --from=build /usr/local/lib/python3.12/dist-packages/torch-${TORCH_VER}.dist-info /usr/local/lib/python3.12/dist-packages/torch-${TORCH_VER}.dist-info COPY --from=build /usr/local/lib/python3.12/dist-packages/torchgen /usr/local/lib/python3.12/dist-packages/torchgen COPY --from=build /usr/local/lib/python3.12/dist-packages/torchvision /usr/local/lib/python3.12/dist-packages/torchvision COPY --from=build /usr/local/lib/python3.12/dist-packages/torchvision-${TORCHVISION_VER}.dist-info /usr/local/lib/python3.12/dist-packages/torchvision-${TORCHVISION_VER}.dist-info COPY --from=build /usr/local/lib/python3.12/dist-packages/torchvision.libs /usr/local/lib/python3.12/dist-packages/torchvision.libs -COPY --from=build /usr/local/lib/python3.12/dist-packages/setuptools /usr/local/lib/python3.12/dist-packages/setuptools -COPY --from=build /usr/local/lib/python3.12/dist-packages/setuptools-${SETUPTOOLS_VER}.dist-info /usr/local/lib/python3.12/dist-packages/setuptools-${SETUPTOOLS_VER}.dist-info COPY --from=build /usr/local/lib/python3.12/dist-packages/functorch /usr/local/lib/python3.12/dist-packages/functorch -COPY --from=build /usr/local/lib/python3.12/dist-packages/triton /usr/local/lib/python3.12/dist-packages/triton -COPY --from=build /usr/local/lib/python3.12/dist-packages/pytorch_triton-${PYTORCH_TRITON_VER}.dist-info /usr/local/lib/python3.12/dist-packages/pytorch_triton-${PYTORCH_TRITON_VER}.dist-info COPY --from=build /usr/local/lib/python3.12/dist-packages/jinja2 /usr/local/lib/python3.12/dist-packages/jinja2 COPY --from=build /usr/local/lib/python3.12/dist-packages/jinja2-${JINJA2_VER}.dist-info /usr/local/lib/python3.12/dist-packages/jinja2-${JINJA2_VER}.dist-info -COPY --from=build /usr/local/lib/python3.12/dist-packages/networkx /usr/local/lib/python3.12/dist-packages/networkx -COPY --from=build /usr/local/lib/python3.12/dist-packages/networkx-${NETWORKX_VER}.dist-info /usr/local/lib/python3.12/dist-packages/networkx-${NETWORKX_VER}.dist-info COPY --from=build /usr/local/lib/python3.12/dist-packages/sympy /usr/local/lib/python3.12/dist-packages/sympy COPY --from=build /usr/local/lib/python3.12/dist-packages/sympy-${SYMPY_VER}.dist-info /usr/local/lib/python3.12/dist-packages/sympy-${SYMPY_VER}.dist-info -COPY --from=build /usr/local/lib/python3.12/dist-packages/packaging /usr/local/lib/python3.12/dist-packages/packaging -COPY --from=build /usr/local/lib/python3.12/dist-packages/packaging-${PACKAGING_VER}.dist-info /usr/local/lib/python3.12/dist-packages/packaging-${PACKAGING_VER}.dist-info COPY --from=build /usr/local/lib/python3.12/dist-packages/flash_attn /usr/local/lib/python3.12/dist-packages/flash_attn COPY --from=build /usr/local/lib/python3.12/dist-packages/flash_attn-${FLASH_ATTN_VER}.dist-info /usr/local/lib/python3.12/dist-packages/flash_attn-${FLASH_ATTN_VER}.dist-info COPY --from=build /usr/local/lib/python3.12/dist-packages/flash_attn_2_cuda.cpython-312-*-linux-gnu.so /usr/local/lib/python3.12/dist-packages/ @@ -478,19 +480,24 @@ COPY --from=dev /workspace/target/release/metrics /usr/local/bin/metrics # NOTE: locking cuda-python version to <13 to avoid breaks with tensorrt-llm 1.0.0rc6. This # can be removed after https://github.com/NVIDIA/TensorRT-LLM/pull/6703 is merged # we upgrade to a published pip wheel containing this change. -RUN uv pip install "cuda-python>=12,<13" && \ - uv pip install --extra-index-url "${TENSORRTLLM_INDEX_URL}" "${TENSORRTLLM_PIP_WHEEL}" && \ - if [ "$ARCH" = "amd64" ]; then \ - pip install "triton==3.3.1"; \ - fi; \ - uv pip install /workspace/wheelhouse/ai_dynamo_runtime*cp312*.whl /workspace/wheelhouse/ai_dynamo*any.whl /workspace/wheelhouse/nixl*.whl +RUN python3 -m pip install --no-cache-dir --break-system-packages "cuda-python>=12,<13" && \ + python3 -m pip install --no-cache-dir --break-system-packages --extra-index-url "${TENSORRTLLM_INDEX_URL}" "${TENSORRTLLM_PIP_WHEEL}" && \ + python3 -m pip install --no-cache-dir --break-system-packages \ + /workspace/wheelhouse/ai_dynamo_runtime*cp312*.whl \ + /workspace/wheelhouse/ai_dynamo*any.whl \ + /workspace/wheelhouse/nixl*.whl && \ + python3 -m pip uninstall -y --break-system-packages triton + # triton is copied from pytorch container below + +COPY --from=build /usr/local/lib/python3.12/dist-packages/triton /usr/local/lib/python3.12/dist-packages/triton +COPY --from=build /usr/local/lib/python3.12/dist-packages/pytorch_triton-${PYTORCH_TRITON_VER}.dist-info /usr/local/lib/python3.12/dist-packages/pytorch_triton-${PYTORCH_TRITON_VER}.dist-info # Copy benchmarks, backends and tests for CI # TODO: Remove this once we have a functional CI image built on top of the runtime image COPY tests /workspace/tests COPY benchmarks /workspace/benchmarks COPY components/backends/trtllm /workspace/components/backends/trtllm -RUN uv pip install /workspace/benchmarks +RUN python3 -m pip install --no-cache-dir --break-system-packages /workspace/benchmarks # Copy files for legal compliance COPY ATTRIBUTION* LICENSE /workspace/ diff --git a/container/deps/requirements.txt b/container/deps/requirements.txt index f5ce8c09cd..b67fb4e079 100644 --- a/container/deps/requirements.txt +++ b/container/deps/requirements.txt @@ -30,7 +30,7 @@ mypy numpy==1.26.4 # pmdarima is not compatible with numpy 2 opentelemetry-api opentelemetry-sdk -pip==25.0.1 +pip pmdarima pre-commit prometheus-api-client From ab4f2fa6c712094270f41442f510f0aadbb94150 Mon Sep 17 00:00:00 2001 From: Yan Ru Pei Date: Tue, 26 Aug 2025 21:18:05 -0700 Subject: [PATCH 44/82] feat: allow specifying consumer name for NATS queue + manually purge old messages (#2740) Signed-off-by: Jason Zhou --- lib/runtime/src/transports/nats.rs | 363 ++++++++++++++++++++++++++++- 1 file changed, 361 insertions(+), 2 deletions(-) diff --git a/lib/runtime/src/transports/nats.rs b/lib/runtime/src/transports/nats.rs index 0313facc70..2fba72cb0e 100644 --- a/lib/runtime/src/transports/nats.rs +++ b/lib/runtime/src/transports/nats.rs @@ -443,6 +443,8 @@ pub struct NatsQueue { subject: String, /// The subscriber for pull-based consumption subscriber: Option, + /// Optional consumer name for broadcast pattern (if None, uses "worker-group") + consumer_name: Option, } impl NatsQueue { @@ -460,6 +462,29 @@ impl NatsQueue { client: None, subject, subscriber: None, + consumer_name: None, + } + } + + /// Create a new NatsQueue with a specific consumer name for broadcast pattern + /// Each consumer with a unique name will receive all messages independently + pub fn new_with_consumer( + stream_name: String, + nats_server: String, + dequeue_timeout: time::Duration, + consumer_name: String, + ) -> Self { + let sanitized_stream_name = stream_name.replace(['/', '\\'], "_"); + let subject = format!("{}.*", sanitized_stream_name); + + Self { + stream_name: sanitized_stream_name, + nats_server, + dequeue_timeout, + client: None, + subject, + subscriber: None, + consumer_name: Some(consumer_name), } } @@ -486,7 +511,11 @@ impl NatsQueue { // Create persistent subscriber let consumer_config = jetstream::consumer::pull::Config { - durable_name: Some("worker-group".to_string()), + durable_name: Some( + self.consumer_name + .clone() + .unwrap_or_else(|| "worker-group".to_string()), + ), ..Default::default() }; @@ -515,6 +544,45 @@ impl NatsQueue { Ok(()) } + /// Shutdown the consumer by deleting it from the stream and closing the connection + /// This permanently removes the consumer from the server + pub async fn shutdown(&mut self) -> Result<()> { + if let (Some(client), Some(consumer_name)) = (&self.client, &self.consumer_name) { + // Get the stream and delete the consumer + let stream = client.jetstream().get_stream(&self.stream_name).await?; + stream.delete_consumer(consumer_name).await.map_err(|e| { + anyhow::anyhow!("Failed to delete consumer {}: {}", consumer_name, e) + })?; + log::debug!( + "Deleted consumer {} from stream {}", + consumer_name, + self.stream_name + ); + } else { + log::warn!( + "Cannot shutdown consumer: client or consumer_name is None (client: {:?}, consumer_name: {:?})", + self.client.is_some(), + self.consumer_name.is_some() + ); + } + + // Then close the connection + self.close().await + } + + /// Count the number of consumers for the stream + pub async fn count_consumers(&mut self) -> Result { + self.ensure_connection().await?; + + if let Some(client) = &self.client { + let mut stream = client.jetstream().get_stream(&self.stream_name).await?; + let info = stream.info().await?; + Ok(info.state.consumer_count) + } else { + Err(anyhow::anyhow!("Client not connected")) + } + } + /// Enqueue a task using the provided data pub async fn enqueue_task(&mut self, task_data: Bytes) -> Result<()> { self.ensure_connection().await?; @@ -564,8 +632,12 @@ impl NatsQueue { if let Some(client) = &self.client { // Get consumer info to get pending messages count let stream = client.jetstream().get_stream(&self.stream_name).await?; + let consumer_name = self + .consumer_name + .clone() + .unwrap_or_else(|| "worker-group".to_string()); let mut consumer: jetstream::consumer::PullConsumer = stream - .get_consumer("worker-group") + .get_consumer(&consumer_name) .await .map_err(|e| anyhow::anyhow!("Failed to get consumer: {}", e))?; let info = consumer.info().await?; @@ -575,6 +647,102 @@ impl NatsQueue { Err(anyhow::anyhow!("Client not connected")) } } + + /// Purge messages from the stream up to (but not including) the specified sequence number + /// This permanently removes messages and affects all consumers of the stream + pub async fn purge_up_to_sequence(&self, sequence: u64) -> Result<()> { + if let Some(client) = &self.client { + let stream = client.jetstream().get_stream(&self.stream_name).await?; + + // NOTE: this purge excludes the sequence itself + // https://docs.rs/nats/latest/nats/jetstream/struct.PurgeRequest.html + stream.purge().sequence(sequence).await.map_err(|e| { + anyhow::anyhow!("Failed to purge stream up to sequence {}: {}", sequence, e) + })?; + + log::debug!( + "Purged stream {} up to sequence {}", + self.stream_name, + sequence + ); + Ok(()) + } else { + Err(anyhow::anyhow!("Client not connected")) + } + } + + /// Purge messages from the stream up to the minimum acknowledged sequence across all consumers + /// This finds the lowest acknowledged sequence number across all consumers and purges up to that point + pub async fn purge_acknowledged(&mut self) -> Result<()> { + self.ensure_connection().await?; + + let Some(client) = &self.client else { + return Err(anyhow::anyhow!("Client not connected")); + }; + + let stream = client.jetstream().get_stream(&self.stream_name).await?; + + // Get all consumer names for the stream + let consumer_names: Vec = stream + .consumer_names() + .try_collect() + .await + .map_err(|e| anyhow::anyhow!("Failed to list consumers: {}", e))?; + + if consumer_names.is_empty() { + log::debug!("No consumers found for stream {}", self.stream_name); + return Ok(()); + } + + // Find the minimum acknowledged sequence across all consumers + let mut min_ack_sequence = u64::MAX; + + for consumer_name in &consumer_names { + let mut consumer: jetstream::consumer::PullConsumer = stream + .get_consumer(consumer_name) + .await + .map_err(|e| anyhow::anyhow!("Failed to get consumer {}: {}", consumer_name, e))?; + + let info = consumer.info().await.map_err(|e| { + anyhow::anyhow!("Failed to get consumer info for {}: {}", consumer_name, e) + })?; + + // The ack_floor contains the stream sequence of the highest contiguously acknowledged message + // If stream_sequence is 0, it means no messages have been acknowledged yet + if info.ack_floor.stream_sequence > 0 { + min_ack_sequence = min_ack_sequence.min(info.ack_floor.stream_sequence); + log::debug!( + "Consumer {} has ack_floor at sequence {}", + consumer_name, + info.ack_floor.stream_sequence + ); + } + } + + // Only purge if we found a valid minimum acknowledged sequence + if min_ack_sequence < u64::MAX && min_ack_sequence > 0 { + // Purge up to (but not including) the minimum acknowledged sequence + 1 + // We add 1 because we want to include the minimum acknowledged message in the purge + let purge_sequence = min_ack_sequence + 1; + + self.purge_up_to_sequence(purge_sequence).await?; + + log::info!( + "Purged stream {} up to acknowledged sequence {} (purged up to sequence {})", + self.stream_name, + min_ack_sequence, + purge_sequence + ); + } else { + log::debug!( + "No messages to purge for stream {} (min_ack_sequence: {})", + self.stream_name, + min_ack_sequence + ); + } + + Ok(()) + } } /// Prometheus metrics that mirror the NATS client statistics (in primitive types) @@ -786,4 +954,195 @@ mod tests { .await .expect("Failed to delete bucket"); } + + // Integration test for broadcast pattern with purging + #[tokio::test] + #[ignore] + async fn test_nats_queue_broadcast_with_purge() { + use uuid::Uuid; + + // Create unique stream name for this test + let stream_name = format!("test-broadcast-{}", Uuid::new_v4()); + let nats_server = "nats://localhost:4222".to_string(); + let timeout = time::Duration::from_secs(0); + + // Create two consumers with different names for the same stream + let consumer1_name = format!("consumer-{}", Uuid::new_v4()); + let consumer2_name = format!("consumer-{}", Uuid::new_v4()); + + let mut queue1 = NatsQueue::new_with_consumer( + stream_name.clone(), + nats_server.clone(), + timeout, + consumer1_name, + ); + + let mut queue2 = NatsQueue::new_with_consumer( + stream_name.clone(), + nats_server.clone(), + timeout, + consumer2_name, + ); + + // Connect both queues (first one creates the stream, second one reuses it) + queue1.connect().await.expect("Failed to connect queue1"); + queue2.connect().await.expect("Failed to connect queue2"); + + // Send 4 messages + let messages = vec![ + Bytes::from("message1"), + Bytes::from("message2"), + Bytes::from("message3"), + Bytes::from("message4"), + ]; + + for msg in &messages { + queue1 + .enqueue_task(msg.clone()) + .await + .expect("Failed to enqueue message"); + } + + // Give JetStream a moment to persist the messages + tokio::time::sleep(time::Duration::from_millis(100)).await; + + // Get stream info to find the sequence numbers + // We need to know the sequence of message 2 to purge up to it + let client_options = Client::builder() + .server(nats_server.clone()) + .build() + .expect("Failed to build client options"); + + let client = client_options + .connect() + .await + .expect("Failed to connect to NATS"); + + // Purge the first two messages (sequence 1 and 2) + // Note: JetStream sequences start at 1, and purge is exclusive of the sequence number + queue1 + .purge_up_to_sequence(3) + .await + .expect("Failed to purge messages"); + + // Give JetStream a moment to process the purge + tokio::time::sleep(time::Duration::from_millis(100)).await; + + // Consumer 1 dequeues one message (message3) + let msg3_consumer1 = queue1 + .dequeue_task(Some(time::Duration::from_millis(500))) + .await + .expect("Failed to dequeue from queue1"); + assert_eq!( + msg3_consumer1, + Some(messages[2].clone()), + "Consumer 1 should get message3" + ); + + // Give JetStream a moment to process acknowledgments + tokio::time::sleep(time::Duration::from_millis(100)).await; + + // Now run purge_acknowledged + // At this point: + // - Consumer 1 has ack'd message 3 (ack_floor = 3) + // - Consumer 2 hasn't consumed anything yet (ack_floor = 0) + // - Min ack_floor = 0, so nothing will be purged + queue1 + .purge_acknowledged() + .await + .expect("Failed to purge acknowledged messages"); + + // Give JetStream a moment to process the purge + tokio::time::sleep(time::Duration::from_millis(100)).await; + + // Now collect remaining messages from both consumers + let mut consumer1_remaining = Vec::new(); + let mut consumer2_remaining = Vec::new(); + + // Collect remaining messages from consumer 1 + while let Some(msg) = queue1 + .dequeue_task(None) + .await + .expect("Failed to dequeue from queue1") + { + consumer1_remaining.push(msg); + } + + // Collect remaining messages from consumer 2 + while let Some(msg) = queue2 + .dequeue_task(None) + .await + .expect("Failed to dequeue from queue2") + { + consumer2_remaining.push(msg); + } + + // Verify consumer 1 gets 1 remaining message (message4) + assert_eq!( + consumer1_remaining.len(), + 1, + "Consumer 1 should have 1 remaining message" + ); + assert_eq!( + consumer1_remaining[0], messages[3], + "Consumer 1 should get message4" + ); + + // Verify consumer 2 gets 2 messages (message3 and message4) + assert_eq!( + consumer2_remaining.len(), + 2, + "Consumer 2 should have 2 messages" + ); + assert_eq!( + consumer2_remaining[0], messages[2], + "Consumer 2 should get message3" + ); + assert_eq!( + consumer2_remaining[1], messages[3], + "Consumer 2 should get message4" + ); + + // Test consumer count and shutdown behavior + // First verify via consumer 1 that there are two consumers + let consumer_count = queue1 + .count_consumers() + .await + .expect("Failed to count consumers"); + assert_eq!(consumer_count, 2, "Should have 2 consumers initially"); + + // Close consumer 1 and verify via consumer 2 that there are still two consumers + queue1.close().await.expect("Failed to close queue1"); + + let consumer_count = queue2 + .count_consumers() + .await + .expect("Failed to count consumers"); + assert_eq!( + consumer_count, 2, + "Should still have 2 consumers after closing queue1" + ); + + // Reconnect queue1 to be able to shutdown + queue1.connect().await.expect("Failed to reconnect queue1"); + + // Shutdown consumer 1 and verify via consumer 2 that there is only one consumer left + queue1.shutdown().await.expect("Failed to shutdown queue1"); + + let consumer_count = queue2 + .count_consumers() + .await + .expect("Failed to count consumers"); + assert_eq!( + consumer_count, 1, + "Should have only 1 consumer after shutting down queue1" + ); + + // Clean up by deleting the stream + client + .jetstream() + .delete_stream(&stream_name) + .await + .expect("Failed to delete test stream"); + } } From ea075a535bdf6384cd069d97bc738014dd4e06af Mon Sep 17 00:00:00 2001 From: Tzu-Ling Kan Date: Tue, 26 Aug 2025 22:50:05 -0700 Subject: [PATCH 45/82] feat: Trtllm metric_labels. (#2666) Signed-off-by: Jason Zhou --- .../backends/trtllm/src/dynamo/trtllm/main.py | 6 +- .../trtllm/src/dynamo/trtllm/publisher.py | 17 +- lib/bindings/python/src/dynamo/_core.pyi | 6 +- tests/serve/test_trtllm.py | 161 ++++++++++++++++++ 4 files changed, 183 insertions(+), 7 deletions(-) diff --git a/components/backends/trtllm/src/dynamo/trtllm/main.py b/components/backends/trtllm/src/dynamo/trtllm/main.py index 6bf5e63137..a14dc41aee 100644 --- a/components/backends/trtllm/src/dynamo/trtllm/main.py +++ b/components/backends/trtllm/src/dynamo/trtllm/main.py @@ -268,16 +268,20 @@ async def init(runtime: DistributedRuntime, config: Config): kv_listener = runtime.namespace(config.namespace).component( config.component ) + metrics_labels = [("model", config.served_model_name)] async with get_publisher( component, engine, kv_listener, int(endpoint.lease_id()), config.kv_block_size, + metrics_labels, ) as publisher: handler_config.publisher = publisher handler = RequestHandlerFactory().get_request_handler(handler_config) - await endpoint.serve_endpoint(handler.generate) + await endpoint.serve_endpoint( + handler.generate, metrics_labels=metrics_labels + ) else: handler = RequestHandlerFactory().get_request_handler(handler_config) await endpoint.serve_endpoint(handler.generate) diff --git a/components/backends/trtllm/src/dynamo/trtllm/publisher.py b/components/backends/trtllm/src/dynamo/trtllm/publisher.py index be252518fb..3a6e2f4af8 100644 --- a/components/backends/trtllm/src/dynamo/trtllm/publisher.py +++ b/components/backends/trtllm/src/dynamo/trtllm/publisher.py @@ -111,13 +111,16 @@ class Publisher: A class to retrieve stats and kv cache events from TRTLLM engine and publish them to the metrics and events publishers. """ - def __init__(self, component, engine, kv_listener, worker_id, kv_block_size): + def __init__( + self, component, engine, kv_listener, worker_id, kv_block_size, metrics_labels + ): self.component = component self.engine = engine self.kv_listener = kv_listener self.worker_id = worker_id self.kv_block_size = kv_block_size self.max_window_size = None + self.metrics_labels = metrics_labels # The first few kv events from the model engine are always "created" type events. # Use these events to capture the max_window_size of the model. @@ -140,7 +143,9 @@ async def _create_metrics_publisher_endpoint(self): if self.metrics_publisher is None: logging.error("KV metrics publisher not initialized!") return - await self.metrics_publisher.create_endpoint(self.component) + await self.metrics_publisher.create_endpoint( + self.component, self.metrics_labels + ) def initialize(self): # Setup the metrics publisher @@ -447,8 +452,12 @@ def should_drop_event(self, event): @asynccontextmanager -async def get_publisher(component, engine, kv_listener, worker_id, kv_block_size): - publisher = Publisher(component, engine, kv_listener, worker_id, kv_block_size) +async def get_publisher( + component, engine, kv_listener, worker_id, kv_block_size, metrics_labels +): + publisher = Publisher( + component, engine, kv_listener, worker_id, kv_block_size, metrics_labels + ) try: publisher.initialize() yield publisher diff --git a/lib/bindings/python/src/dynamo/_core.pyi b/lib/bindings/python/src/dynamo/_core.pyi index 73e89a5f34..839b4ba649 100644 --- a/lib/bindings/python/src/dynamo/_core.pyi +++ b/lib/bindings/python/src/dynamo/_core.pyi @@ -9,6 +9,7 @@ from typing import ( Dict, List, Optional, + Tuple, Union, ) @@ -216,7 +217,7 @@ class Endpoint: ... - async def serve_endpoint(self, handler: RequestHandler, graceful_shutdown: bool = True) -> None: + async def serve_endpoint(self, handler: RequestHandler, graceful_shutdown: bool = True, metrics_labels: Optional[List[Tuple[str, str]]] = None) -> None: """ Serve an endpoint discoverable by all connected clients at `{{ namespace }}/components/{{ component_name }}/endpoints/{{ endpoint_name }}` @@ -224,6 +225,7 @@ class Endpoint: Args: handler: The request handler function graceful_shutdown: Whether to wait for inflight requests to complete during shutdown (default: True) + metrics_labels: Optional list of metrics labels to add to the metrics """ ... @@ -438,7 +440,7 @@ class WorkerMetricsPublisher: Create a `WorkerMetricsPublisher` object """ - def create_endpoint(self, component: Component) -> None: + def create_endpoint(self, component: Component, metrics_labels: Optional[List[Tuple[str, str]]] = None) -> None: """ Similar to Component.create_service, but only service created through this method will interact with KV router of the same component. diff --git a/tests/serve/test_trtllm.py b/tests/serve/test_trtllm.py index 898c3e3b6a..c6e747f138 100644 --- a/tests/serve/test_trtllm.py +++ b/tests/serve/test_trtllm.py @@ -174,3 +174,164 @@ def test_deployment(trtllm_config_test, request, runtime_services): url, payload=request_body, timeout=config.timeout - elapsed ) server_process.check_response(payload, response, response_handler) + + +@pytest.mark.e2e +@pytest.mark.gpu_1 +@pytest.mark.trtllm_marker +@pytest.mark.slow +def test_metrics_labels(request, runtime_services): + """ + Test that the trtllm backend correctly exports model labels in its metrics. + + This test uses the --extra-engine-args flag with agg.yaml configuration + to start the backend without needing a pre-built TensorRT-LLM engine. + + Prerequisites: + - etcd and NATS must be running (docker compose -f deploy/docker-compose.yml up -d) + - The test runs from the trtllm directory to access engine_configs/agg.yaml + """ + import os + import re + import subprocess + import threading + + import requests + + logger = logging.getLogger(request.node.name) + logger.info("Starting test_metrics_labels") + + # Use the exact configuration that works for the user + model_path = "Qwen/Qwen3-0.6B" + served_model_name = "Qwen/Qwen3-0.6B" + agg_engine_args = "engine_configs/agg.yaml" + metrics_port = 8081 + timeout = 60 + + # Change to the trtllm directory where engine_configs/agg.yaml exists + + working_directory = os.path.abspath("components/backends/trtllm") + + # Build command using the user's working command + command = [ + "python3", + "-m", + "dynamo.trtllm", + "--model-path", + model_path, + "--served-model-name", + served_model_name, + "--extra-engine-args", + agg_engine_args, + "--max-seq-len", + "100", + "--max-num-tokens", + "100", + "--publish-events-and-metrics", + ] + + # Set environment for metrics + env = os.environ.copy() + env["DYN_SYSTEM_ENABLED"] = "true" + env["DYN_SYSTEM_PORT"] = str(metrics_port) + + # Start the backend process + logger.info(f"Starting trtllm backend with model: {served_model_name}") + logger.info(f"Command: {' '.join(command)}") + logger.info(f"Working directory: {working_directory}") + process = subprocess.Popen( + command, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + cwd=working_directory, + ) + + try: + # Start a thread to capture and log output + output_lines = [] + + def log_output(): + if process.stdout is None: + logger.warning("Process stdout is None, cannot capture output") + return + for line in process.stdout: + line = line.strip() + if line: + output_lines.append(line) + logger.info(f"[TRTLLM] {line}") + + output_thread = threading.Thread(target=log_output) + output_thread.daemon = True + output_thread.start() + + # Wait for metrics endpoint to be ready + metrics_url = f"http://localhost:{metrics_port}/metrics" + start_time = time.time() + + while time.time() - start_time < timeout: + # Check if process has died + if process.poll() is not None: + logger.error(f"Process exited with code: {process.returncode}") + logger.error("Last 20 output lines:\n" + "\n".join(output_lines[-20:])) + pytest.fail( + f"trtllm backend process died with exit code {process.returncode}" + ) + + try: + response = requests.get(metrics_url, timeout=5) + if response.status_code == 200: + logger.info("Metrics endpoint is ready") + break + except requests.RequestException as e: + logger.debug(f"Metrics not ready yet: {e}") + time.sleep(2) + else: + logger.error("Last 50 output lines:\n" + "\n".join(output_lines[-50:])) + pytest.fail( + f"Metrics endpoint did not become available within {timeout} seconds" + ) + + # Check that the metrics include the model label + response = requests.get(metrics_url) + assert response.status_code == 200, "Failed to fetch metrics" + + metrics_text = response.text + logger.info(f"Metrics text: {metrics_text}") + + # With the --extra-engine-args flag pointing to agg.yaml, + # the backend should be able to start properly and register endpoints. + # Let's check for the dynamo_component_requests_total metric with our model label. + + # Parse the Prometheus metrics to find our label + pattern = rf'dynamo_component_requests_total\{{[^}}]*model="{re.escape(served_model_name)}"[^}}]*\}}\s+(\d+)' + matches = re.findall(pattern, metrics_text) + + if matches: + initial_value = int(matches[0]) + assert ( + initial_value == 0 + ), f"Expected initial metric value to be 0, got {initial_value}" + else: + # Check if any dynamo_component metrics exist + if "dynamo_component" in metrics_text: + logger.info( + "✓ Metrics endpoint is working (found dynamo_component metrics)" + ) + logger.warning( + "Note: dynamo_component_requests_total not found - likely because dummy engine didn't fully initialize" + ) + logger.info("For complete testing, use a real pre-built TRT-LLM engine") + else: + pytest.fail("No dynamo_component metrics found at all") + + finally: + # Clean up + logger.info("Terminating backend process") + process.terminate() + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() From d92f7bb69c46c29f07c802657de66a7422fdf76a Mon Sep 17 00:00:00 2001 From: GuanLuo <41310872+GuanLuo@users.noreply.github.com> Date: Tue, 26 Aug 2025 22:57:31 -0700 Subject: [PATCH 46/82] feat: KServe gRPC support (#2638) Signed-off-by: Jason Zhou --- .github/workflows/pre-merge-rust.yml | 4 +- Cargo.lock | 745 +++--- .../frontend/src/dynamo/frontend/main.py | 8 + launch/dynamo-run/src/lib.rs | 1 - launch/dynamo-run/src/main.rs | 2 +- lib/bindings/python/Cargo.lock | 256 +- lib/llm/Cargo.toml | 10 + lib/llm/build.rs | 8 +- lib/llm/src/entrypoint/input.rs | 9 + lib/llm/src/entrypoint/input/grpc.rs | 170 ++ lib/llm/src/grpc.rs | 4 + lib/llm/src/grpc/protos/kserve.proto | 624 +++++ lib/llm/src/grpc/protos/model_config.proto | 2140 +++++++++++++++++ lib/llm/src/grpc/service.rs | 5 + lib/llm/src/grpc/service/kserve.rs | 625 +++++ lib/llm/src/grpc/service/openai.rs | 198 ++ lib/llm/src/http/service/openai.rs | 12 +- lib/llm/src/lib.rs | 1 + lib/llm/tests/kserve_service.rs | 1055 ++++++++ 19 files changed, 5374 insertions(+), 503 deletions(-) create mode 100644 lib/llm/src/entrypoint/input/grpc.rs create mode 100644 lib/llm/src/grpc.rs create mode 100644 lib/llm/src/grpc/protos/kserve.proto create mode 100644 lib/llm/src/grpc/protos/model_config.proto create mode 100644 lib/llm/src/grpc/service.rs create mode 100644 lib/llm/src/grpc/service/kserve.rs create mode 100644 lib/llm/src/grpc/service/openai.rs create mode 100644 lib/llm/tests/kserve_service.rs diff --git a/.github/workflows/pre-merge-rust.yml b/.github/workflows/pre-merge-rust.yml index f8ab1f2290..998ad32fee 100644 --- a/.github/workflows/pre-merge-rust.yml +++ b/.github/workflows/pre-merge-rust.yml @@ -100,11 +100,11 @@ jobs: # Have an explicit step to build tests first to separate time spent on build vs execution. - name: Compile Tests working-directory: ${{ matrix.dir }} - run: cargo test --locked --no-run + run: cargo test --locked --no-run --target-dir ${HOME}/tmp || df -h - name: Run Doc Tests working-directory: ${{ matrix.dir }} run: cargo doc --no-deps && cargo test --locked --doc - name: Run Unit Tests working-directory: ${{ matrix.dir }} # NOTE: --all-targets doesn't run doc tests - run: cargo test --locked --all-targets + run: cargo test --locked --all-targets --target-dir ${HOME}/tmp diff --git a/Cargo.lock b/Cargo.lock index 8326fa6e2a..60535ddb5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,7 +33,7 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "getrandom 0.3.3", "once_cell", "serde", @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -118,29 +118,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "apodize" @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -180,7 +180,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -257,7 +257,7 @@ checksum = "0289cba6d5143bfe8251d57b4a8cac036adf158525a76533a7082ba65ec76398" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -279,18 +279,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -439,7 +439,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "itoa", "matchit 0.8.4", @@ -508,7 +508,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -522,7 +522,7 @@ dependencies = [ "fs-err", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "pin-project-lite", "rustls", @@ -554,7 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "miniz_oxide", "object", @@ -601,10 +601,10 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -614,7 +614,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.104", + "syn 2.0.106", "which", ] @@ -624,10 +624,10 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -635,7 +635,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -644,10 +644,10 @@ version = "0.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -655,7 +655,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -723,9 +723,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bitstream-io" @@ -742,7 +742,7 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "constant_time_eq", ] @@ -793,7 +793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.10", "serde", ] @@ -817,22 +817,22 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -865,9 +865,9 @@ dependencies = [ "ahash", "cached_proc_macro", "cached_proc_macro_types", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "once_cell", - "thiserror 2.0.12", + "thiserror 2.0.16", "web-time", ] @@ -880,7 +880,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -930,7 +930,7 @@ dependencies = [ "rand 0.9.2", "rand_distr 0.5.1", "rayon", - "safetensors 0.6.1", + "safetensors 0.6.2", "thiserror 1.0.69", "ug", "ug-cuda", @@ -970,7 +970,7 @@ dependencies = [ "metal 0.27.0", "num-traits", "rayon", - "safetensors 0.6.1", + "safetensors 0.6.2", "serde", "thiserror 1.0.69", ] @@ -996,24 +996,24 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" dependencies = [ - "clap 4.5.42", + "clap 4.5.45", "heck 0.4.1", - "indexmap 2.10.0", + "indexmap 2.11.0", "log", "proc-macro2", "quote", "serde", "serde_json", - "syn 2.0.104", + "syn 2.0.106", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.2.33" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -1047,9 +1047,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -1063,7 +1063,7 @@ version = "0.13.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe45e18904af7af10e4312df7c97251e98af98c70f42f1f2587aecfcbee56bf" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "lazy_static", "num-traits", "regex", @@ -1110,9 +1110,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.42" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -1120,9 +1120,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.42" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -1133,14 +1133,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1177,7 +1177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "itoa", "rustversion", "ryu", @@ -1301,7 +1301,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -1457,7 +1457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1506,7 +1506,7 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", "curve25519-dalek-derive", "digest", @@ -1523,7 +1523,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1571,7 +1571,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1593,7 +1593,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1611,7 +1611,7 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -1659,7 +1659,7 @@ checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1670,18 +1670,18 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1702,7 +1702,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1712,7 +1712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1723,7 +1723,7 @@ checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1743,7 +1743,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1755,8 +1755,8 @@ dependencies = [ "anyhow", "bytemuck", "bytemuck_derive", - "hashbrown 0.15.4", - "regex-syntax 0.8.5", + "hashbrown 0.15.5", + "regex-syntax 0.8.6", "strum", ] @@ -1837,7 +1837,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1904,12 +1904,12 @@ dependencies = [ "eventsource-stream", "futures", "rand 0.9.2", - "reqwest 0.12.22", + "reqwest 0.12.23", "reqwest-eventsource", "secrecy", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-test", @@ -1941,7 +1941,7 @@ dependencies = [ "dynamo-llm", "dynamo-runtime", "either", - "indexmap 2.10.0", + "indexmap 2.11.0", "mistralrs", "serde_json", "tokio", @@ -2002,10 +2002,11 @@ dependencies = [ "parking_lot", "prometheus", "proptest", + "prost", "rand 0.9.2", "rayon", "regex", - "reqwest 0.12.22", + "reqwest 0.12.23", "rmp-serde", "rstest 0.18.2", "rstest_reuse", @@ -2015,19 +2016,21 @@ dependencies = [ "serial_test", "strum", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "tmq", "tokenizers", "tokio", "tokio-stream", "tokio-util", - "toktrie 1.1.1", - "toktrie_hf_tokenizers 1.1.1", + "toktrie 1.2.0", + "toktrie_hf_tokenizers 1.2.0", + "tonic 0.13.1", + "tonic-build", "tower-http", "tracing", "unicode-segmentation", "url", - "uuid 1.17.0", + "uuid 1.18.0", "validator", "xxhash-rust", "zeromq", @@ -2045,7 +2048,7 @@ dependencies = [ "serde", "serde_json", "tracing", - "uuid 1.17.0", + "uuid 1.18.0", ] [[package]] @@ -2055,7 +2058,7 @@ dependencies = [ "anyhow", "async-stream", "async-trait", - "clap 4.5.42", + "clap 4.5.45", "dynamo-async-openai", "dynamo-engine-llamacpp", "dynamo-engine-mistralrs", @@ -2073,7 +2076,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", - "uuid 1.17.0", + "uuid 1.18.0", "vergen-gitcl", ] @@ -2114,7 +2117,7 @@ dependencies = [ "prometheus", "rand 0.9.2", "regex", - "reqwest 0.12.22", + "reqwest 0.12.23", "rstest 0.23.0", "serde", "serde_json", @@ -2122,7 +2125,7 @@ dependencies = [ "stdio-override", "temp-env", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-util", @@ -2130,7 +2133,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", - "uuid 1.17.0", + "uuid 1.18.0", "validator", "xxhash-rust", ] @@ -2176,7 +2179,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2206,7 +2209,7 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -2224,7 +2227,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2244,7 +2247,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2264,7 +2267,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2307,7 +2310,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2410,8 +2413,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" dependencies = [ "bit-set 0.5.3", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -2421,8 +2424,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set 0.8.0", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -2538,7 +2541,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2555,9 +2558,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2670,7 +2673,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2990,7 +2993,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -3003,7 +3006,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "js-sys", "libc", "r-efi", @@ -3030,7 +3033,7 @@ checksum = "3ac5654356c6f7f6116905aeaf92ab002c3d03414ada5dbe0bb2e32aa5fea173" dependencies = [ "fancy-regex 0.14.0", "ggml-quants", - "indexmap 2.10.0", + "indexmap 2.11.0", "log", "num_enum", ] @@ -3053,9 +3056,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -3066,8 +3069,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -3082,7 +3085,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.10.0", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -3091,9 +3094,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -3101,7 +3104,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.10.0", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -3121,7 +3124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "bytemuck", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "crunchy", "num-traits", "rand 0.9.2", @@ -3142,9 +3145,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -3211,10 +3214,10 @@ dependencies = [ "log", "num_cpus", "rand 0.9.2", - "reqwest 0.12.22", + "reqwest 0.12.23", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "ureq", "windows-sys 0.60.2", @@ -3243,7 +3246,7 @@ checksum = "c1637acec3b965bab873352189d887b12c87b4f8d7571f4d185e796be5654ad8" dependencies = [ "html5ever 0.31.0", "tendril", - "thiserror 2.0.12", + "thiserror 2.0.16", "unicode-width 0.2.1", ] @@ -3371,20 +3374,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.11", + "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3397,7 +3402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "rustls", "rustls-native-certs 0.8.1", @@ -3414,7 +3419,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "pin-project-lite", "tokio", @@ -3429,7 +3434,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -3450,7 +3455,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", @@ -3581,9 +3586,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3652,12 +3657,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "serde", ] @@ -3704,7 +3709,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -3715,7 +3720,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3733,12 +3738,12 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", + "bitflags 2.9.3", + "cfg-if 1.0.3", "libc", ] @@ -3800,6 +3805,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -3836,14 +3850,14 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -3875,7 +3889,7 @@ dependencies = [ "anyhow", "base64 0.21.7", "bytecount", - "clap 4.5.42", + "clap 4.5.45", "fancy-regex 0.11.0", "fraction", "getrandom 0.2.16", @@ -3892,7 +3906,7 @@ dependencies = [ "serde_json", "time", "url", - "uuid 1.17.0", + "uuid 1.18.0", ] [[package]] @@ -3935,9 +3949,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libdynamo_llm" @@ -3957,7 +3971,7 @@ dependencies = [ "tokio-stream", "tracing", "tracing-subscriber", - "uuid 1.17.0", + "uuid 1.18.0", ] [[package]] @@ -3976,7 +3990,7 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "windows-targets 0.53.3", ] @@ -3992,7 +4006,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", ] @@ -4048,8 +4062,8 @@ source = "git+https://github.com/guidance-ai/llguidance.git?rev=c432092#c432092d dependencies = [ "anyhow", "derivre", - "indexmap 2.10.0", - "regex-syntax 0.8.5", + "indexmap 2.11.0", + "regex-syntax 0.8.6", "serde", "serde_json", "toktrie 1.0.0", @@ -4063,7 +4077,7 @@ checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" dependencies = [ "libc", "neli", - "thiserror 2.0.12", + "thiserror 2.0.16", "windows-sys 0.59.0", ] @@ -4175,7 +4189,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4215,7 +4229,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "rayon", ] @@ -4227,9 +4241,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", "stable_deref_trait", @@ -4256,7 +4270,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -4271,7 +4285,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -4285,16 +4299,16 @@ name = "metrics" version = "0.4.1" dependencies = [ "axum 0.8.4", - "clap 4.5.42", + "clap 4.5.45", "dynamo-llm", "dynamo-runtime", "futures", "prometheus", "rand 0.9.2", - "reqwest 0.12.22", + "reqwest 0.12.23", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -4317,9 +4331,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e60ac08614cc09062820e51d5d94c2fce16b94ea4e5003bb81b99a95f84e876" +checksum = "a9f264d75233323f4b7d2f03aefe8a990690cdebfbfe26ea86bcbaec5e9ac990" dependencies = [ "memo-map", "self_cell", @@ -4329,9 +4343,9 @@ dependencies = [ [[package]] name = "minijinja-contrib" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e5bfa889f16d8c10ec92ac964074a68a7206c0fd9748ff23a31942c85d97c" +checksum = "182ba1438db4679ddfa03792c183bdc2b9ce26b58e7d41a749e59b06497cf136" dependencies = [ "minijinja", "serde", @@ -4415,14 +4429,14 @@ dependencies = [ "anyhow", "candle-core 0.9.1 (git+https://github.com/EricLBuehler/candle.git?rev=95d713f9)", "candle-nn", - "clap 4.5.42", + "clap 4.5.45", "either", "futures", "image", - "indexmap 2.10.0", + "indexmap 2.11.0", "mistralrs-core", "rand 0.9.2", - "reqwest 0.12.22", + "reqwest 0.12.23", "serde", "serde_json", "tokio", @@ -4460,7 +4474,7 @@ dependencies = [ "candle-nn", "cfgrammar", "chrono", - "clap 4.5.42", + "clap 4.5.45", "csv", "derive-new", "derive_more 2.0.1", @@ -4470,13 +4484,13 @@ dependencies = [ "futures", "galil-seiferas", "half 2.6.0", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "hf-hub", "hound", "html2text", "http 1.3.1", "image", - "indexmap 2.10.0", + "indexmap 2.11.0", "indicatif", "interprocess", "itertools 0.14.0", @@ -4502,13 +4516,13 @@ dependencies = [ "rand_isaac", "rayon", "regex", - "regex-automata 0.4.9", - "reqwest 0.12.22", + "regex-automata 0.4.10", + "reqwest 0.12.23", "rubato", "rust-mcp-schema", "rustc-hash 2.1.1", "rustfft", - "safetensors 0.6.1", + "safetensors 0.6.2", "schemars 0.8.22", "scraper", "serde", @@ -4520,7 +4534,7 @@ dependencies = [ "strum", "symphonia", "sysinfo", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokenizers", "tokio", "tokio-rayon", @@ -4531,7 +4545,7 @@ dependencies = [ "tracing", "tracing-subscriber", "urlencoding", - "uuid 1.17.0", + "uuid 1.18.0", "variantly", "vob", ] @@ -4545,7 +4559,7 @@ dependencies = [ "async-trait", "futures-util", "http 1.3.1", - "reqwest 0.12.22", + "reqwest 0.12.23", "rust-mcp-schema", "serde", "serde_json", @@ -4553,7 +4567,7 @@ dependencies = [ "tokio-tungstenite 0.24.0", "tracing", "utoipa", - "uuid 1.17.0", + "uuid 1.18.0", ] [[package]] @@ -4568,7 +4582,7 @@ dependencies = [ "half 2.6.0", "metal 0.27.0", "once_cell", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -4590,10 +4604,10 @@ dependencies = [ "paste", "rayon", "regex", - "safetensors 0.6.1", + "safetensors 0.6.2", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "yoke 0.7.5", @@ -4627,7 +4641,7 @@ checksum = "c402a4092d5e204f32c9e155431046831fa712637043c58cb73bc6bc6c9663b5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4754,7 +4768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "memoffset", "pin-utils", @@ -4766,8 +4780,8 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", + "bitflags 2.9.3", + "cfg-if 1.0.3", "cfg_aliases", "libc", ] @@ -4784,7 +4798,7 @@ dependencies = [ "os_info", "pkg-config", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", ] @@ -4916,7 +4930,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4989,7 +5003,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5069,7 +5083,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", "once_cell", "onig_sys", @@ -5100,19 +5114,19 @@ dependencies = [ "anyhow", "base64 0.22.1", "bstr", - "clap 4.5.42", + "clap 4.5.45", "fancy-regex 0.13.0", "futures", "image", "regex", - "reqwest 0.12.22", + "reqwest 0.12.23", "rustc-hash 1.1.0", "serde", "serde_json", "serde_with", "sha1", "sha2", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -5121,8 +5135,8 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", + "bitflags 2.9.3", + "cfg-if 1.0.3", "foreign-types 0.3.2", "libc", "once_cell", @@ -5138,7 +5152,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5228,7 +5242,7 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "redox_syscall", "smallvec", @@ -5261,7 +5275,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5275,9 +5289,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" @@ -5286,7 +5300,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", "ucd-trie", ] @@ -5310,7 +5324,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5330,7 +5344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.11.0", ] [[package]] @@ -5373,7 +5387,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5402,7 +5416,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5440,7 +5454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap 2.10.0", + "indexmap 2.11.0", "quick-xml", "serde", "time", @@ -5534,12 +5548,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5579,14 +5593,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -5599,7 +5613,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "version_check", "yansi", ] @@ -5620,7 +5634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5629,13 +5643,13 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "fnv", "lazy_static", "memchr", "parking_lot", "protobuf", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -5646,13 +5660,13 @@ checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.9.1", + "bitflags 2.9.3", "lazy_static", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", "rusty-fork", "tempfile", "unarray", @@ -5684,7 +5698,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.104", + "syn 2.0.106", "tempfile", ] @@ -5698,7 +5712,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5749,7 +5763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" dependencies = [ "bytemuck", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libm", "num-complex", "reborrow", @@ -5779,9 +5793,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.0" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] @@ -5800,7 +5814,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2 0.5.10", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -5821,7 +5835,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -5975,7 +5989,7 @@ dependencies = [ "av1-grain", "bitstream-io", "built", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "interpolate_name", "itertools 0.12.1", "libc", @@ -6028,7 +6042,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -6039,9 +6053,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -6060,9 +6074,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -6095,7 +6109,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -6106,7 +6120,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -6126,19 +6140,19 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -6152,13 +6166,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] @@ -6169,9 +6183,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "relative-path" @@ -6217,9 +6231,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -6227,11 +6241,11 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.11", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls", "hyper-tls", "hyper-util", @@ -6277,7 +6291,7 @@ dependencies = [ "mime", "nom 7.1.3", "pin-project-lite", - "reqwest 0.12.22", + "reqwest 0.12.23", "thiserror 1.0.69", ] @@ -6294,7 +6308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "getrandom 0.2.16", "libc", "untrusted", @@ -6353,14 +6367,14 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "glob", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", "unicode-ident", ] @@ -6370,7 +6384,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "glob", "proc-macro-crate", "proc-macro2", @@ -6378,7 +6392,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", "unicode-ident", ] @@ -6390,7 +6404,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" dependencies = [ "quote", "rand 0.8.5", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6472,7 +6486,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6485,7 +6499,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.9.4", @@ -6530,7 +6544,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.3.0", ] [[package]] @@ -6576,9 +6590,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" @@ -6619,9 +6633,9 @@ dependencies = [ [[package]] name = "safetensors" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acc594eede59bd7facbb55c179d1d67980f412c201544b21fce7f9ac3aab4a5" +checksum = "172dd94c5a87b5c79f945c863da53b2ebc7ccef4eca24ac63cca66a41aab2178" dependencies = [ "serde", "serde_json", @@ -6638,9 +6652,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] @@ -6699,7 +6713,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6745,7 +6759,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -6754,11 +6768,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6781,7 +6795,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cssparser", "derive_more 0.99.20", "fxhash", @@ -6848,7 +6862,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6859,16 +6873,16 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "itoa", "memchr", "ryu", @@ -6911,7 +6925,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6945,7 +6959,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.0", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -6964,7 +6978,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6973,7 +6987,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "itoa", "ryu", "serde", @@ -7002,7 +7016,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7020,7 +7034,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", "digest", ] @@ -7031,7 +7045,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", "digest", ] @@ -7080,9 +7094,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -7151,9 +7165,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -7330,7 +7344,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7487,9 +7501,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -7519,7 +7533,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7528,7 +7542,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "byteorder", "enum-as-inner", "libc", @@ -7542,7 +7556,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "byteorder", "enum-as-inner", "libc", @@ -7556,7 +7570,7 @@ version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "core-foundation-sys", "libc", "ntapi", @@ -7582,7 +7596,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -7638,15 +7652,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7662,12 +7676,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7690,11 +7704,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -7705,18 +7719,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7725,7 +7739,7 @@ version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -7794,9 +7808,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -7845,11 +7859,11 @@ dependencies = [ "rayon", "rayon-cond", "regex", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.12", + "thiserror 2.0.16", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -7884,7 +7898,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7967,16 +7981,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", - "hashbrown 0.15.4", "pin-project-lite", "tokio", ] @@ -8016,9 +8029,9 @@ dependencies = [ [[package]] name = "toktrie" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c01fe70e9a91498c029fb6d5aacf9a648bb1bf30a5db4c344d3effb6367b1f3" +checksum = "804459e7818d5cc8920f80d555526454353117a4f2fcda38e4b38666639d7e81" dependencies = [ "anyhow", "bytemuck", @@ -8042,16 +8055,16 @@ dependencies = [ [[package]] name = "toktrie_hf_tokenizers" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759491ad9b56050f817e24d68f48eb7f13bee5f8b48127ba3a9079a579726f40" +checksum = "6fc302ed2c53b1d7fcbe3d760a2dc6567def0eb2da7ca09cb21a16b9a9aa4f13" dependencies = [ "anyhow", "log", "serde", "serde_json", "tokenizers", - "toktrie 1.1.1", + "toktrie 1.2.0", ] [[package]] @@ -8081,7 +8094,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "serde", "serde_spanned", "toml_datetime", @@ -8106,11 +8119,11 @@ dependencies = [ "axum 0.7.9", "base64 0.22.1", "bytes", - "h2 0.4.11", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -8135,11 +8148,11 @@ dependencies = [ "axum 0.8.4", "base64 0.22.1", "bytes", - "h2 0.4.11", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -8166,7 +8179,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8197,7 +8210,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.10.0", + "indexmap 2.11.0", "pin-project-lite", "slab", "sync_wrapper 1.0.2", @@ -8214,7 +8227,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytes", "futures-util", "http 1.3.1", @@ -8270,7 +8283,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8379,7 +8392,7 @@ dependencies = [ "bytes", "log", "rand 0.9.2", - "thiserror 2.0.12", + "thiserror 2.0.16", "utf-8", ] @@ -8548,9 +8561,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -8588,7 +8601,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "serde", "serde_json", "utoipa-gen", @@ -8602,7 +8615,7 @@ checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8616,9 +8629,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -8664,7 +8677,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8744,9 +8757,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vob" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0baa046ba374a7701d98032a468a0bbd968a8cd3a2ae39c94d74e211fac05c81" +checksum = "bc936b5a7202a703aeaf7ce05e7931db2e0c8126813f97db3e9e06d867b0bb38" dependencies = [ "num-traits", "serde", @@ -8801,7 +8814,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -8817,7 +8830,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -8827,7 +8840,7 @@ version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "js-sys", "once_cell", "wasm-bindgen", @@ -8852,7 +8865,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8993,11 +9006,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -9046,7 +9059,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9057,7 +9070,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9319,9 +9332,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -9332,7 +9345,7 @@ version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "windows-sys 0.48.0", ] @@ -9342,7 +9355,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] @@ -9405,7 +9418,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -9417,7 +9430,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -9438,7 +9451,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9458,7 +9471,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -9492,7 +9505,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-util", - "uuid 1.17.0", + "uuid 1.18.0", ] [[package]] @@ -9518,9 +9531,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke 0.8.0", "zerofrom", @@ -9535,7 +9548,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9548,7 +9561,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap 2.10.0", + "indexmap 2.11.0", "num_enum", "thiserror 1.0.69", ] diff --git a/components/frontend/src/dynamo/frontend/main.py b/components/frontend/src/dynamo/frontend/main.py index 7960e4faa1..ca3d6d342c 100644 --- a/components/frontend/src/dynamo/frontend/main.py +++ b/components/frontend/src/dynamo/frontend/main.py @@ -176,6 +176,12 @@ def parse_args(): default=None, help="Prefix for Dynamo frontend metrics. If unset, uses DYN_METRICS_PREFIX env var or 'dynamo_frontend'.", ) + parser.add_argument( + "--kserve-grpc-server", + action="store_true", + default=False, + help="Start KServe gRPC server.", + ) flags = parser.parse_args() @@ -246,6 +252,8 @@ async def async_main(): try: if flags.interactive: await run_input(runtime, "text", engine) + elif flags.kserve_grpc_server: + await run_input(runtime, "grpc", engine) else: await run_input(runtime, "http", engine) except asyncio.exceptions.CancelledError: diff --git a/launch/dynamo-run/src/lib.rs b/launch/dynamo-run/src/lib.rs index e078bfdf1a..50bb7491d1 100644 --- a/launch/dynamo-run/src/lib.rs +++ b/launch/dynamo-run/src/lib.rs @@ -83,7 +83,6 @@ pub async fn run( rt.clone(), ) .await?; - // // Run in from an input // diff --git a/launch/dynamo-run/src/main.rs b/launch/dynamo-run/src/main.rs index 5a2e39f34d..b1e4d73ff0 100644 --- a/launch/dynamo-run/src/main.rs +++ b/launch/dynamo-run/src/main.rs @@ -26,7 +26,7 @@ Example: See `docs/guides/dynamo_run.md` in the repo for full details. "#; -const USAGE: &str = "USAGE: dynamo-run in=[http|text|dyn://|batch:] out=ENGINE_LIST|auto|dyn:// [--http-port 8080] [--model-path ] [--model-name ] [--model-config ] [--context-length=N] [--kv-cache-block-size=16] [--extra-engine-args=args.json] [--static-worker] [--router-mode random|round-robin|kv] [--kv-overlap-score-weight=2.0] [--router-temperature=0.0] [--use-kv-events] [--max-num-batched-tokens=1.0] [--migration-limit=0] [--verbosity (-v|-vv)]"; +const USAGE: &str = "USAGE: dynamo-run in=[http|grpc|text|dyn://|batch:] out=ENGINE_LIST|auto|dyn:// [--http-port 8080] [--model-path ] [--model-name ] [--model-config ] [--context-length=N] [--kv-cache-block-size=16] [--extra-engine-args=args.json] [--static-worker] [--router-mode random|round-robin|kv] [--kv-overlap-score-weight=2.0] [--router-temperature=0.0] [--use-kv-events] [--max-num-batched-tokens=1.0] [--migration-limit=0] [--verbosity (-v|-vv)]"; fn main() -> anyhow::Result<()> { // Set log level based on verbosity flag diff --git a/lib/bindings/python/Cargo.lock b/lib/bindings/python/Cargo.lock index e48c2a59dc..9f5ef6d25e 100644 --- a/lib/bindings/python/Cargo.lock +++ b/lib/bindings/python/Cargo.lock @@ -23,7 +23,7 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "getrandom 0.3.3", "once_cell", "serde", @@ -465,7 +465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "miniz_oxide", "object", @@ -506,7 +506,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "cexpr", "clang-sys", "itertools 0.12.1", @@ -529,7 +529,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "cexpr", "clang-sys", "itertools 0.13.0", @@ -587,9 +587,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bitstream-io" @@ -606,7 +606,7 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "constant_time_eq", ] @@ -637,7 +637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.10", "serde", ] @@ -727,9 +727,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -763,9 +763,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -865,7 +865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "itoa", "rustversion", "ryu", @@ -948,7 +948,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -1047,7 +1047,7 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", "curve25519-dalek-derive", "digest", @@ -1117,7 +1117,7 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -1342,7 +1342,7 @@ dependencies = [ "secrecy", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-util", @@ -1394,7 +1394,9 @@ dependencies = [ "nixl-sys", "offset-allocator", "oneshot", + "parking_lot", "prometheus", + "prost", "rand 0.9.2", "rayon", "regex", @@ -1404,7 +1406,7 @@ dependencies = [ "serde_json", "strum", "tempfile", - "thiserror 2.0.15", + "thiserror 2.0.16", "tmq", "tokenizers", "tokio", @@ -1412,6 +1414,8 @@ dependencies = [ "tokio-util", "toktrie", "toktrie_hf_tokenizers", + "tonic", + "tonic-build", "tower-http", "tracing", "unicode-segmentation", @@ -1460,7 +1464,7 @@ dependencies = [ "rstest", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-util", @@ -1505,7 +1509,7 @@ dependencies = [ "serde", "serde_json", "socket2 0.5.10", - "thiserror 2.0.15", + "thiserror 2.0.16", "tokio", "tokio-stream", "tokio-util", @@ -1573,7 +1577,7 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -1733,8 +1737,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" dependencies = [ "bit-set 0.5.3", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -1744,8 +1748,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set 0.8.0", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -1810,9 +1814,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2207,7 +2211,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -2220,7 +2224,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "js-sys", "libc", "r-efi", @@ -2247,7 +2251,7 @@ checksum = "3ac5654356c6f7f6116905aeaf92ab002c3d03414ada5dbe0bb2e32aa5fea173" dependencies = [ "fancy-regex 0.14.0", "ggml-quants", - "indexmap 2.10.0", + "indexmap 2.11.0", "log", "num_enum", ] @@ -2286,7 +2290,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -2300,7 +2304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "bytemuck", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "crunchy", "num-traits", "rand 0.9.2", @@ -2360,7 +2364,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.16", "tokio", "ureq", "windows-sys 0.60.2", @@ -2625,9 +2629,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2696,9 +2700,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -2736,7 +2740,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -2752,21 +2756,21 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.2", - "cfg-if 1.0.1", + "bitflags 2.9.3", + "cfg-if 1.0.3", "libc", ] @@ -2836,9 +2840,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -2920,7 +2924,7 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "windows-targets 0.53.3", ] @@ -2936,7 +2940,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "libc", ] @@ -2966,7 +2970,7 @@ checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" dependencies = [ "libc", "neli", - "thiserror 2.0.15", + "thiserror 2.0.16", "windows-sys 0.59.0", ] @@ -3048,7 +3052,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "rayon", ] @@ -3060,9 +3064,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", "stable_deref_trait", @@ -3110,9 +3114,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e60ac08614cc09062820e51d5d94c2fce16b94ea4e5003bb81b99a95f84e876" +checksum = "a9f264d75233323f4b7d2f03aefe8a990690cdebfbfe26ea86bcbaec5e9ac990" dependencies = [ "memo-map", "self_cell", @@ -3121,9 +3125,9 @@ dependencies = [ [[package]] name = "minijinja-contrib" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e5bfa889f16d8c10ec92ac964074a68a7206c0fd9748ff23a31942c85d97c" +checksum = "182ba1438db4679ddfa03792c183bdc2b9ce26b58e7d41a749e59b06497cf136" dependencies = [ "minijinja", "serde", @@ -3289,7 +3293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "memoffset 0.7.1", "pin-utils", @@ -3301,8 +3305,8 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.2", - "cfg-if 1.0.1", + "bitflags 2.9.3", + "cfg-if 1.0.3", "cfg_aliases", "libc", ] @@ -3319,7 +3323,7 @@ dependencies = [ "os_info", "pkg-config", "serde", - "thiserror 2.0.15", + "thiserror 2.0.16", "tracing", ] @@ -3561,7 +3565,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "libc", "once_cell", "onig_sys", @@ -3598,7 +3602,7 @@ dependencies = [ "serde_with", "sha1", "sha2", - "thiserror 2.0.15", + "thiserror 2.0.16", ] [[package]] @@ -3653,7 +3657,7 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "redox_syscall", "smallvec", @@ -3700,9 +3704,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -3711,7 +3715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.11.0", ] [[package]] @@ -3769,7 +3773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap 2.10.0", + "indexmap 2.11.0", "quick-xml", "serde", "time", @@ -3915,13 +3919,13 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "fnv", "lazy_static", "memchr", "parking_lot", "protobuf", - "thiserror 2.0.15", + "thiserror 2.0.16", ] [[package]] @@ -4015,7 +4019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" dependencies = [ "bytemuck", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libm", "num-complex", "reborrow", @@ -4028,7 +4032,7 @@ version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "indoc", "libc", "memoffset 0.9.1", @@ -4140,9 +4144,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.1" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] @@ -4161,7 +4165,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2 0.5.10", - "thiserror 2.0.15", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -4182,7 +4186,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.15", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -4298,7 +4302,7 @@ dependencies = [ "av1-grain", "bitstream-io", "built", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "interpolate_name", "itertools 0.12.1", "libc", @@ -4351,7 +4355,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -4403,7 +4407,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -4414,7 +4418,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.15", + "thiserror 2.0.16", ] [[package]] @@ -4439,14 +4443,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -4460,13 +4464,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] @@ -4477,9 +4481,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "relative-path" @@ -4563,7 +4567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "getrandom 0.2.16", "libc", "untrusted", @@ -4610,7 +4614,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "glob", "proc-macro-crate", "proc-macro2", @@ -4655,7 +4659,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4668,7 +4672,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.9.4", @@ -4843,7 +4847,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4856,7 +4860,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4917,7 +4921,7 @@ version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "itoa", "memchr", "ryu", @@ -4985,7 +4989,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.0", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -5013,7 +5017,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", "digest", ] @@ -5024,7 +5028,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", "digest", ] @@ -5254,7 +5258,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "byteorder", "enum-as-inner", "libc", @@ -5268,7 +5272,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "byteorder", "enum-as-inner", "libc", @@ -5282,7 +5286,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5318,15 +5322,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5340,11 +5344,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.15" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.15", + "thiserror-impl 2.0.16", ] [[package]] @@ -5360,9 +5364,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.15" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -5375,7 +5379,7 @@ version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -5485,11 +5489,11 @@ dependencies = [ "rayon", "rayon-cond", "regex", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.15", + "thiserror 2.0.16", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -5637,7 +5641,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "serde", "serde_spanned", "toml_datetime", @@ -5703,7 +5707,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.10.0", + "indexmap 2.11.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5720,7 +5724,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "bytes", "futures-util", "http", @@ -5957,9 +5961,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -6090,7 +6094,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -6116,7 +6120,7 @@ version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "js-sys", "once_cell", "wasm-bindgen", @@ -6254,11 +6258,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6495,9 +6499,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -6508,7 +6512,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -6714,7 +6718,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap 2.10.0", + "indexmap 2.11.0", "num_enum", "thiserror 1.0.69", ] diff --git a/lib/llm/Cargo.toml b/lib/llm/Cargo.toml index 0f9a8c08b4..806463b738 100644 --- a/lib/llm/Cargo.toml +++ b/lib/llm/Cargo.toml @@ -110,6 +110,13 @@ tower-http = {workspace = true} rustls = { version = "0.23" } +# grpc-service +# ping version to 0.13.1 so it depends on prost 0.13.5 +# which is used across other libraries +tonic = { version = "0.13.1" } +# Request prost specifically so tonic-build properly compiles protobuf message +prost = { version = "0.13.5" } + # tokenizers tokenizers = { version = "0.21.4", default-features = false, features = [ "onig", @@ -157,3 +164,6 @@ insta = { version = "1.41", features = [ ] } aligned-vec = "0.6.4" lazy_static = "1.4" + +[build-dependencies] +tonic-build = { version = "0.13.1"} \ No newline at end of file diff --git a/lib/llm/build.rs b/lib/llm/build.rs index 602ce351ec..216f43337f 100644 --- a/lib/llm/build.rs +++ b/lib/llm/build.rs @@ -13,8 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -fn main() { +fn main() -> Result<(), Box> { println!("cargo:warning=Building with CUDA KV off"); + build_protos() +} + +fn build_protos() -> Result<(), Box> { + tonic_build::compile_protos("src/grpc/protos/kserve.proto")?; + Ok(()) } // NOTE: Preserving this build.rs for reference. We may want to re-enable diff --git a/lib/llm/src/entrypoint/input.rs b/lib/llm/src/entrypoint/input.rs index d12174a6f0..b207e05c52 100644 --- a/lib/llm/src/entrypoint/input.rs +++ b/lib/llm/src/entrypoint/input.rs @@ -18,6 +18,7 @@ pub mod batch; mod common; pub use common::build_routed_pipeline; pub mod endpoint; +pub mod grpc; pub mod http; pub mod text; @@ -43,6 +44,9 @@ pub enum Input { /// Batch mode. Run all the prompts, write the outputs, exit. Batch(PathBuf), + + // Run an KServe compatible gRPC server + Grpc, } impl FromStr for Input { @@ -59,6 +63,7 @@ impl TryFrom<&str> for Input { fn try_from(s: &str) -> anyhow::Result { match s { "http" => Ok(Input::Http), + "grpc" => Ok(Input::Grpc), "text" => Ok(Input::Text), "stdin" => Ok(Input::Stdin), endpoint_path if endpoint_path.starts_with(ENDPOINT_SCHEME) => { @@ -77,6 +82,7 @@ impl fmt::Display for Input { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match self { Input::Http => "http", + Input::Grpc => "grpc", Input::Text => "text", Input::Stdin => "stdin", Input::Endpoint(path) => path, @@ -113,6 +119,9 @@ pub async fn run_input( Input::Http => { http::run(runtime, engine_config).await?; } + Input::Grpc => { + grpc::run(runtime, engine_config).await?; + } Input::Text => { text::run(runtime, None, engine_config).await?; } diff --git a/lib/llm/src/entrypoint/input/grpc.rs b/lib/llm/src/entrypoint/input/grpc.rs new file mode 100644 index 0000000000..7b1181ebd0 --- /dev/null +++ b/lib/llm/src/entrypoint/input/grpc.rs @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use crate::{ + discovery::{MODEL_ROOT_PATH, ModelManager, ModelWatcher}, + engines::StreamingEngineAdapter, + entrypoint::{self, EngineConfig, input::common}, + grpc::service::kserve, + kv_router::KvRouterConfig, + types::openai::{ + chat_completions::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse}, + completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, + }, +}; +use dynamo_runtime::transports::etcd; +use dynamo_runtime::{DistributedRuntime, Runtime}; +use dynamo_runtime::{distributed::DistributedConfig, pipeline::RouterMode}; + +/// Build and run an KServe gRPC service +pub async fn run(runtime: Runtime, engine_config: EngineConfig) -> anyhow::Result<()> { + let mut grpc_service_builder = kserve::KserveService::builder() + .port(engine_config.local_model().http_port()) // [WIP] generalize port.. + .with_request_template(engine_config.local_model().request_template()); + + let grpc_service = match engine_config { + EngineConfig::Dynamic(_) => { + let distributed_runtime = DistributedRuntime::from_settings(runtime.clone()).await?; + let etcd_client = distributed_runtime.etcd_client(); + // This allows the /health endpoint to query etcd for active instances + grpc_service_builder = grpc_service_builder.with_etcd_client(etcd_client.clone()); + let grpc_service = grpc_service_builder.build()?; + match etcd_client { + Some(ref etcd_client) => { + let router_config = engine_config.local_model().router_config(); + // Listen for models registering themselves in etcd, add them to gRPC service + run_watcher( + distributed_runtime, + grpc_service.state().manager_clone(), + etcd_client.clone(), + MODEL_ROOT_PATH, + router_config.router_mode, + Some(router_config.kv_router_config), + router_config.busy_threshold, + ) + .await?; + } + None => { + // Static endpoints don't need discovery + } + } + grpc_service + } + EngineConfig::StaticRemote(local_model) => { + let card = local_model.card(); + let router_mode = local_model.router_config().router_mode; + + let dst_config = DistributedConfig::from_settings(true); // true means static + let distributed_runtime = DistributedRuntime::new(runtime.clone(), dst_config).await?; + let grpc_service = grpc_service_builder.build()?; + let manager = grpc_service.model_manager(); + + let endpoint_id = local_model.endpoint_id(); + let component = distributed_runtime + .namespace(&endpoint_id.namespace)? + .component(&endpoint_id.component)?; + let client = component.endpoint(&endpoint_id.name).client().await?; + + let kv_chooser = if router_mode == RouterMode::KV { + Some( + manager + .kv_chooser_for( + local_model.display_name(), + &component, + card.kv_cache_block_size, + Some(local_model.router_config().kv_router_config), + ) + .await?, + ) + } else { + None + }; + + let chat_engine = entrypoint::build_routed_pipeline::< + NvCreateChatCompletionRequest, + NvCreateChatCompletionStreamResponse, + >(card, &client, router_mode, None, kv_chooser.clone()) + .await?; + manager.add_chat_completions_model(local_model.display_name(), chat_engine)?; + + let completions_engine = entrypoint::build_routed_pipeline::< + NvCreateCompletionRequest, + NvCreateCompletionResponse, + >(card, &client, router_mode, None, kv_chooser) + .await?; + manager.add_completions_model(local_model.display_name(), completions_engine)?; + + grpc_service + } + EngineConfig::StaticFull { engine, model, .. } => { + let grpc_service = grpc_service_builder.build()?; + let engine = Arc::new(StreamingEngineAdapter::new(engine)); + let manager = grpc_service.model_manager(); + manager.add_completions_model(model.service_name(), engine.clone())?; + manager.add_chat_completions_model(model.service_name(), engine)?; + grpc_service + } + EngineConfig::StaticCore { + engine: inner_engine, + model, + .. + } => { + let grpc_service = grpc_service_builder.build()?; + let manager = grpc_service.model_manager(); + + let chat_pipeline = common::build_pipeline::< + NvCreateChatCompletionRequest, + NvCreateChatCompletionStreamResponse, + >(model.card(), inner_engine.clone()) + .await?; + manager.add_chat_completions_model(model.service_name(), chat_pipeline)?; + + let cmpl_pipeline = common::build_pipeline::< + NvCreateCompletionRequest, + NvCreateCompletionResponse, + >(model.card(), inner_engine) + .await?; + manager.add_completions_model(model.service_name(), cmpl_pipeline)?; + grpc_service + } + }; + grpc_service.run(runtime.primary_token()).await?; + runtime.shutdown(); // Cancel primary token + Ok(()) +} + +/// Spawns a task that watches for new models in etcd at network_prefix, +/// and registers them with the ModelManager so that the HTTP service can use them. +async fn run_watcher( + runtime: DistributedRuntime, + model_manager: Arc, + etcd_client: etcd::Client, + network_prefix: &str, + router_mode: RouterMode, + kv_router_config: Option, + busy_threshold: Option, +) -> anyhow::Result<()> { + let watch_obj = ModelWatcher::new( + runtime, + model_manager, + router_mode, + kv_router_config, + busy_threshold, + ); + tracing::info!("Watching for remote model at {network_prefix}"); + let models_watcher = etcd_client.kv_get_and_watch_prefix(network_prefix).await?; + let (_prefix, _watcher, receiver) = models_watcher.dissolve(); + + // [gluo NOTE] This is different from http::run_watcher where it alters the HTTP service + // endpoint being exposed, gRPC doesn't have the same concept as the KServe service + // only has one kind of inference endpoint. + + // Pass the sender to the watcher + let _watcher_task = tokio::spawn(async move { + watch_obj.watch(receiver).await; + }); + + Ok(()) +} diff --git a/lib/llm/src/grpc.rs b/lib/llm/src/grpc.rs new file mode 100644 index 0000000000..c7c4aa1009 --- /dev/null +++ b/lib/llm/src/grpc.rs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +pub mod service; diff --git a/lib/llm/src/grpc/protos/kserve.proto b/lib/llm/src/grpc/protos/kserve.proto new file mode 100644 index 0000000000..b9efb9cefd --- /dev/null +++ b/lib/llm/src/grpc/protos/kserve.proto @@ -0,0 +1,624 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package inference; + +//@@.. cpp:namespace:: inference + +import "model_config.proto"; + +//@@ +//@@.. cpp:var:: service InferenceService +//@@ +//@@ Inference Server GRPC endpoints. +//@@ +service GRPCInferenceService +{ + //@@ .. cpp:var:: rpc ModelMetadata(ModelMetadataRequest) returns + //@@ (ModelMetadataResponse) + //@@ + //@@ Get model metadata. + //@@ + rpc ModelMetadata(ModelMetadataRequest) returns (ModelMetadataResponse) {} + + //@@ .. cpp:var:: rpc ModelInfer(ModelInferRequest) returns + //@@ (ModelInferResponse) + //@@ + //@@ Perform inference using a specific model. + //@@ + rpc ModelInfer(ModelInferRequest) returns (ModelInferResponse) {} + + //@@ .. cpp:var:: rpc ModelStreamInfer(stream ModelInferRequest) returns + //@@ (stream ModelStreamInferResponse) + //@@ + //@@ Perform streaming inference. + //@@ + rpc ModelStreamInfer(stream ModelInferRequest) returns (stream ModelStreamInferResponse) {} + + //@@ .. cpp:var:: rpc ModelConfig(ModelConfigRequest) returns + //@@ (ModelConfigResponse) + //@@ + //@@ Get model configuration. + //@@ + rpc ModelConfig(ModelConfigRequest) returns (ModelConfigResponse) {} +} + +//@@ +//@@.. cpp:var:: message ModelMetadataRequest +//@@ +//@@ Request message for ModelMetadata. +//@@ +message ModelMetadataRequest +{ + //@@ + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the model. + //@@ + string name = 1; + + //@@ .. cpp:var:: string version + //@@ + //@@ The version of the model to check for readiness. If not + //@@ given the server will choose a version based on the + //@@ model and internal policy. + //@@ + string version = 2; +} + +//@@ +//@@.. cpp:var:: message ModelMetadataResponse +//@@ +//@@ Response message for ModelMetadata. +//@@ +message ModelMetadataResponse +{ + //@@ + //@@ .. cpp:var:: message TensorMetadata + //@@ + //@@ Metadata for a tensor. + //@@ + message TensorMetadata + { + //@@ + //@@ .. cpp:var:: string name + //@@ + //@@ The tensor name. + //@@ + string name = 1; + + //@@ + //@@ .. cpp:var:: string datatype + //@@ + //@@ The tensor data type. + //@@ + string datatype = 2; + + //@@ + //@@ .. cpp:var:: int64 shape (repeated) + //@@ + //@@ The tensor shape. A variable-size dimension is represented + //@@ by a -1 value. + //@@ + repeated int64 shape = 3; + } + + //@@ + //@@ .. cpp:var:: string name + //@@ + //@@ The model name. + //@@ + string name = 1; + + //@@ + //@@ .. cpp:var:: string versions (repeated) + //@@ + //@@ The versions of the model. + //@@ + repeated string versions = 2; + + //@@ + //@@ .. cpp:var:: string platform + //@@ + //@@ The model's platform. + //@@ + string platform = 3; + + //@@ + //@@ .. cpp:var:: TensorMetadata inputs (repeated) + //@@ + //@@ The model's inputs. + //@@ + repeated TensorMetadata inputs = 4; + + //@@ + //@@ .. cpp:var:: TensorMetadata outputs (repeated) + //@@ + //@@ The model's outputs. + //@@ + repeated TensorMetadata outputs = 5; +} + +//@@ +//@@.. cpp:var:: message InferParameter +//@@ +//@@ An inference parameter value. +//@@ +message InferParameter +{ + //@@ .. cpp:var:: oneof parameter_choice + //@@ + //@@ The parameter value can be a string, an int64, + //@@ an uint64, a double, or a boolean + //@@ + //@@ Note: double and uint64 are currently + //@@ placeholders for future use and + //@@ are not supported for custom parameters + //@@ + oneof parameter_choice + { + //@@ .. cpp:var:: bool bool_param + //@@ + //@@ A boolean parameter value. + //@@ + bool bool_param = 1; + + //@@ .. cpp:var:: int64 int64_param + //@@ + //@@ An int64 parameter value. + //@@ + int64 int64_param = 2; + + //@@ .. cpp:var:: string string_param + //@@ + //@@ A string parameter value. + //@@ + string string_param = 3; + + //@@ .. cpp:var:: double double_param + //@@ + //@@ A double parameter value. + //@@ + double double_param = 4; + + //@@ .. cpp:var:: uint64 uint64_param + //@@ + //@@ A uint64 parameter value. + //@@ + //@@ Not supported for custom parameters + //@@ + uint64 uint64_param = 5; + } +} + +//@@ +//@@.. cpp:var:: message InferTensorContents +//@@ +//@@ The data contained in a tensor represented by the repeated type +//@@ that matches the tensor's data type. Protobuf oneof is not used +//@@ because oneofs cannot contain repeated fields. +//@@ +message InferTensorContents +{ + //@@ + //@@ .. cpp:var:: bool bool_contents (repeated) + //@@ + //@@ Representation for BOOL data type. The size must match what is + //@@ expected by the tensor's shape. The contents must be the flattened, + //@@ one-dimensional, row-major order of the tensor elements. + //@@ + repeated bool bool_contents = 1; + + //@@ + //@@ .. cpp:var:: int32 int_contents (repeated) + //@@ + //@@ Representation for INT8, INT16, and INT32 data types. The size + //@@ must match what is expected by the tensor's shape. The contents + //@@ must be the flattened, one-dimensional, row-major order of the + //@@ tensor elements. + //@@ + repeated int32 int_contents = 2; + + //@@ + //@@ .. cpp:var:: int64 int64_contents (repeated) + //@@ + //@@ Representation for INT64 data types. The size must match what + //@@ is expected by the tensor's shape. The contents must be the + //@@ flattened, one-dimensional, row-major order of the tensor elements. + //@@ + repeated int64 int64_contents = 3; + + //@@ + //@@ .. cpp:var:: uint32 uint_contents (repeated) + //@@ + //@@ Representation for UINT8, UINT16, and UINT32 data types. The size + //@@ must match what is expected by the tensor's shape. The contents + //@@ must be the flattened, one-dimensional, row-major order of the + //@@ tensor elements. + //@@ + repeated uint32 uint_contents = 4; + + //@@ + //@@ .. cpp:var:: uint64 uint64_contents (repeated) + //@@ + //@@ Representation for UINT64 data types. The size must match what + //@@ is expected by the tensor's shape. The contents must be the + //@@ flattened, one-dimensional, row-major order of the tensor elements. + //@@ + repeated uint64 uint64_contents = 5; + + //@@ + //@@ .. cpp:var:: float fp32_contents (repeated) + //@@ + //@@ Representation for FP32 data type. The size must match what is + //@@ expected by the tensor's shape. The contents must be the flattened, + //@@ one-dimensional, row-major order of the tensor elements. + //@@ + repeated float fp32_contents = 6; + + //@@ + //@@ .. cpp:var:: double fp64_contents (repeated) + //@@ + //@@ Representation for FP64 data type. The size must match what is + //@@ expected by the tensor's shape. The contents must be the flattened, + //@@ one-dimensional, row-major order of the tensor elements. + //@@ + repeated double fp64_contents = 7; + + //@@ + //@@ .. cpp:var:: bytes bytes_contents (repeated) + //@@ + //@@ Representation for BYTES data type. The size must match what is + //@@ expected by the tensor's shape. The contents must be the flattened, + //@@ one-dimensional, row-major order of the tensor elements. + //@@ + repeated bytes bytes_contents = 8; +} + +//@@ +//@@.. cpp:var:: message ModelInferRequest +//@@ +//@@ Request message for ModelInfer. +//@@ +message ModelInferRequest +{ + //@@ + //@@ .. cpp:var:: message InferInputTensor + //@@ + //@@ An input tensor for an inference request. + //@@ + message InferInputTensor + { + //@@ + //@@ .. cpp:var:: string name + //@@ + //@@ The tensor name. + //@@ + string name = 1; + + //@@ + //@@ .. cpp:var:: string datatype + //@@ + //@@ The tensor data type. + //@@ + string datatype = 2; + + //@@ + //@@ .. cpp:var:: int64 shape (repeated) + //@@ + //@@ The tensor shape. + //@@ + repeated int64 shape = 3; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ Optional inference input tensor parameters. + //@@ + map parameters = 4; + + //@@ .. cpp:var:: InferTensorContents contents + //@@ + //@@ The tensor contents using a data-type format. This field + //@@ must not be specified if tensor contents are being specified + //@@ in ModelInferRequest.raw_input_contents. + //@@ + InferTensorContents contents = 5; + } + + //@@ + //@@ .. cpp:var:: message InferRequestedOutputTensor + //@@ + //@@ An output tensor requested for an inference request. + //@@ + message InferRequestedOutputTensor + { + //@@ + //@@ .. cpp:var:: string name + //@@ + //@@ The tensor name. + //@@ + string name = 1; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ Optional requested output tensor parameters. + //@@ + map parameters = 2; + } + + //@@ .. cpp:var:: string model_name + //@@ + //@@ The name of the model to use for inferencing. + //@@ + string model_name = 1; + + //@@ .. cpp:var:: string model_version + //@@ + //@@ The version of the model to use for inference. If not + //@@ given the latest/most-recent version of the model is used. + //@@ + string model_version = 2; + + //@@ .. cpp:var:: string id + //@@ + //@@ Optional identifier for the request. If specified will be + //@@ returned in the response. + //@@ + string id = 3; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ Optional inference parameters. + //@@ + map parameters = 4; + + //@@ + //@@ .. cpp:var:: InferInputTensor inputs (repeated) + //@@ + //@@ The input tensors for the inference. + //@@ + repeated InferInputTensor inputs = 5; + + //@@ + //@@ .. cpp:var:: InferRequestedOutputTensor outputs (repeated) + //@@ + //@@ The requested output tensors for the inference. Optional, if not + //@@ specified all outputs specified in the model config will be + //@@ returned. + //@@ + repeated InferRequestedOutputTensor outputs = 6; + + //@@ + //@@ .. cpp:var:: bytes raw_input_contents + //@@ + //@@ The data contained in an input tensor can be represented in + //@@ "raw" bytes form or in the repeated type that matches the + //@@ tensor's data type. Using the "raw" bytes form will + //@@ typically allow higher performance due to the way protobuf + //@@ allocation and reuse interacts with GRPC. For example, see + //@@ https://github.com/grpc/grpc/issues/23231. + //@@ + //@@ To use the raw representation 'raw_input_contents' must be + //@@ initialized with data for each tensor in the same order as + //@@ 'inputs'. For each tensor, the size of this content must + //@@ match what is expected by the tensor's shape and data + //@@ type. The raw data must be the flattened, one-dimensional, + //@@ row-major order of the tensor elements without any stride + //@@ or padding between the elements. Note that the FP16 and BF16 data + //@@ types must be represented as raw content as there is no + //@@ specific data type for a 16-bit float type. + //@@ + //@@ If this field is specified then InferInputTensor::contents + //@@ must not be specified for any input tensor. + //@@ + repeated bytes raw_input_contents = 7; +} + +//@@ +//@@.. cpp:var:: message ModelInferResponse +//@@ +//@@ Response message for ModelInfer. +//@@ +message ModelInferResponse +{ + //@@ + //@@ .. cpp:var:: message InferOutputTensor + //@@ + //@@ An output tensor returned for an inference request. + //@@ + message InferOutputTensor + { + //@@ + //@@ .. cpp:var:: string name + //@@ + //@@ The tensor name. + //@@ + string name = 1; + + //@@ + //@@ .. cpp:var:: string datatype + //@@ + //@@ The tensor data type. + //@@ + string datatype = 2; + + //@@ + //@@ .. cpp:var:: int64 shape (repeated) + //@@ + //@@ The tensor shape. + //@@ + repeated int64 shape = 3; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ Optional output tensor parameters. + //@@ + map parameters = 4; + + //@@ .. cpp:var:: InferTensorContents contents + //@@ + //@@ The tensor contents using a data-type format. This field + //@@ must not be specified if tensor contents are being specified + //@@ in ModelInferResponse.raw_output_contents. + //@@ + InferTensorContents contents = 5; + } + + //@@ .. cpp:var:: string model_name + //@@ + //@@ The name of the model used for inference. + //@@ + string model_name = 1; + + //@@ .. cpp:var:: string model_version + //@@ + //@@ The version of the model used for inference. + //@@ + string model_version = 2; + + //@@ .. cpp:var:: string id + //@@ + //@@ The id of the inference request if one was specified. + //@@ + string id = 3; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ Optional inference response parameters. + //@@ + map parameters = 4; + + //@@ + //@@ .. cpp:var:: InferOutputTensor outputs (repeated) + //@@ + //@@ The output tensors holding inference results. + //@@ + repeated InferOutputTensor outputs = 5; + + //@@ + //@@ .. cpp:var:: bytes raw_output_contents + //@@ + //@@ The data contained in an output tensor can be represented in + //@@ "raw" bytes form or in the repeated type that matches the + //@@ tensor's data type. Using the "raw" bytes form will + //@@ typically allow higher performance due to the way protobuf + //@@ allocation and reuse interacts with GRPC. For example, see + //@@ https://github.com/grpc/grpc/issues/23231. + //@@ + //@@ To use the raw representation 'raw_output_contents' must be + //@@ initialized with data for each tensor in the same order as + //@@ 'outputs'. For each tensor, the size of this content must + //@@ match what is expected by the tensor's shape and data + //@@ type. The raw data must be the flattened, one-dimensional, + //@@ row-major order of the tensor elements without any stride + //@@ or padding between the elements. Note that the FP16 and BF16 data + //@@ types must be represented as raw content as there is no + //@@ specific data type for a 16-bit float type. + //@@ + //@@ If this field is specified then InferOutputTensor::contents + //@@ must not be specified for any output tensor. + //@@ + repeated bytes raw_output_contents = 6; +} + +//@@ +//@@.. cpp:var:: message ModelStreamInferResponse +//@@ +//@@ Response message for ModelStreamInfer. +//@@ +message ModelStreamInferResponse +{ + //@@ + //@@ .. cpp:var:: string error_message + //@@ + //@@ The message describing the error. The empty message + //@@ indicates the inference was successful without errors. + //@@ + string error_message = 1; + + //@@ + //@@ .. cpp:var:: ModelInferResponse infer_response + //@@ + //@@ Holds the results of the request. + //@@ + ModelInferResponse infer_response = 2; +} + +//@@ +//@@.. cpp:var:: message ModelConfigRequest +//@@ +//@@ Request message for ModelConfig. +//@@ +message ModelConfigRequest +{ + //@@ + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the model. + //@@ + string name = 1; + + //@@ .. cpp:var:: string version + //@@ + //@@ The version of the model. If not given the model version + //@@ is selected automatically based on the version policy. + //@@ + string version = 2; +} + +//@@ +//@@.. cpp:var:: message ModelConfigResponse +//@@ +//@@ Response message for ModelConfig. +//@@ +message ModelConfigResponse +{ + //@@ + //@@ .. cpp:var:: ModelConfig config + //@@ + //@@ The model configuration. + //@@ + ModelConfig config = 1; +} + +//@@ +//@@.. cpp:var:: message ModelRepositoryParameter +//@@ +//@@ An model repository parameter value. +//@@ +message ModelRepositoryParameter +{ + //@@ .. cpp:var:: oneof parameter_choice + //@@ + //@@ The parameter value can be a string, an int64 or + //@@ a boolean + //@@ + oneof parameter_choice + { + //@@ .. cpp:var:: bool bool_param + //@@ + //@@ A boolean parameter value. + //@@ + bool bool_param = 1; + + //@@ .. cpp:var:: int64 int64_param + //@@ + //@@ An int64 parameter value. + //@@ + int64 int64_param = 2; + + //@@ .. cpp:var:: string string_param + //@@ + //@@ A string parameter value. + //@@ + string string_param = 3; + + //@@ .. cpp:var:: bytes bytes_param + //@@ + //@@ A bytes parameter value. + //@@ + bytes bytes_param = 4; + } +} diff --git a/lib/llm/src/grpc/protos/model_config.proto b/lib/llm/src/grpc/protos/model_config.proto new file mode 100644 index 0000000000..e3555ac9c2 --- /dev/null +++ b/lib/llm/src/grpc/protos/model_config.proto @@ -0,0 +1,2140 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package inference; + +//@@.. cpp:namespace:: inference + +//@@ +//@@.. cpp:enum:: DataType +//@@ +//@@ Data types supported for input and output tensors. +//@@ +enum DataType { + //@@ .. cpp:enumerator:: DataType::INVALID = 0 + TYPE_INVALID = 0; + + //@@ .. cpp:enumerator:: DataType::BOOL = 1 + TYPE_BOOL = 1; + + //@@ .. cpp:enumerator:: DataType::UINT8 = 2 + TYPE_UINT8 = 2; + //@@ .. cpp:enumerator:: DataType::UINT16 = 3 + TYPE_UINT16 = 3; + //@@ .. cpp:enumerator:: DataType::UINT32 = 4 + TYPE_UINT32 = 4; + //@@ .. cpp:enumerator:: DataType::UINT64 = 5 + TYPE_UINT64 = 5; + + //@@ .. cpp:enumerator:: DataType::INT8 = 6 + TYPE_INT8 = 6; + //@@ .. cpp:enumerator:: DataType::INT16 = 7 + TYPE_INT16 = 7; + //@@ .. cpp:enumerator:: DataType::INT32 = 8 + TYPE_INT32 = 8; + //@@ .. cpp:enumerator:: DataType::INT64 = 9 + TYPE_INT64 = 9; + + //@@ .. cpp:enumerator:: DataType::FP16 = 10 + TYPE_FP16 = 10; + //@@ .. cpp:enumerator:: DataType::FP32 = 11 + TYPE_FP32 = 11; + //@@ .. cpp:enumerator:: DataType::FP64 = 12 + TYPE_FP64 = 12; + + //@@ .. cpp:enumerator:: DataType::STRING = 13 + TYPE_STRING = 13; + + //@@ .. cpp:enumerator:: DataType::BF16 = 14 + TYPE_BF16 = 14; +} + +//@@ +//@@ .. cpp:var:: message ModelRateLimiter +//@@ +//@@ The specifications required by the rate limiter to properly +//@@ schedule the inference requests across the different models +//@@ and their instances. +//@@ +message ModelRateLimiter +{ + //@@ .. cpp:var:: message Resource + //@@ + //@@ The resource property. + //@@ + message Resource + { + //@@ .. cpp:var:: string name + //@@ + //@@ The name associated with the resource. + //@@ + string name = 1; + + //@@ .. cpp:var:: bool global + //@@ + //@@ Whether or not the resource is global. If true then the resource + //@@ is assumed to be shared among the devices otherwise specified + //@@ count of the resource is assumed for each device associated + //@@ with the instance. + //@@ + bool global = 2; + + //@@ .. cpp:var:: uint32 count + //@@ + //@@ The number of resources required for the execution of the model + //@@ instance. + //@@ + uint32 count = 3; + } + + //@@ .. cpp:var:: Resource resources (repeated) + //@@ + //@@ The resources required to execute the request on a model instance. + //@@ Resources are just names with a corresponding count. The execution + //@@ of the instance will be blocked until the specified resources are + //@@ available. By default an instance uses no rate-limiter resources. + //@@ + repeated Resource resources = 1; + + //@@ .. cpp:var:: uint32 priority + //@@ + //@@ The optional weighting value to be used for prioritizing across + //@@ instances. An instance with priority 2 will be given 1/2 the + //@@ number of scheduling chances as an instance_group with priority + //@@ 1. The default priority is 1. The priority of value 0 will be + //@@ treated as priority 1. + //@@ + uint32 priority = 2; +} + +//@@ +//@@.. cpp:var:: message ModelInstanceGroup +//@@ +//@@ A group of one or more instances of a model and resources made +//@@ available for those instances. +//@@ +message ModelInstanceGroup +{ + //@@ + //@@ .. cpp:enum:: Kind + //@@ + //@@ Kind of this instance group. + //@@ + enum Kind { + //@@ .. cpp:enumerator:: Kind::KIND_AUTO = 0 + //@@ + //@@ This instance group represents instances that can run on either + //@@ CPU or GPU. If all GPUs listed in 'gpus' are available then + //@@ instances will be created on GPU(s), otherwise instances will + //@@ be created on CPU. + //@@ + KIND_AUTO = 0; + + //@@ .. cpp:enumerator:: Kind::KIND_GPU = 1 + //@@ + //@@ This instance group represents instances that must run on the + //@@ GPU. + //@@ + KIND_GPU = 1; + + //@@ .. cpp:enumerator:: Kind::KIND_CPU = 2 + //@@ + //@@ This instance group represents instances that must run on the + //@@ CPU. + //@@ + KIND_CPU = 2; + + //@@ .. cpp:enumerator:: Kind::KIND_MODEL = 3 + //@@ + //@@ This instance group represents instances that should run on the + //@@ CPU and/or GPU(s) as specified by the model or backend itself. + //@@ The inference server will not override the model/backend + //@@ settings. + //@@ + KIND_MODEL = 3; + } + + //@@ + //@@ .. cpp:var:: message SecondaryDevice + //@@ + //@@ A secondary device required for a model instance. + //@@ + message SecondaryDevice + { + //@@ + //@@ .. cpp:enum:: SecondaryDeviceKind + //@@ + //@@ The kind of the secondary device. + //@@ + enum SecondaryDeviceKind { + //@@ .. cpp:enumerator:: SecondaryDeviceKind::KIND_NVDLA = 0 + //@@ + //@@ An NVDLA core. http://nvdla.org + //@@ Currently KIND_NVDLA is only supported by the TensorRT backend. + //@@ + KIND_NVDLA = 0; + } + + //@@ .. cpp:var:: SecondaryDeviceKind kind + //@@ + //@@ The secondary device kind. + //@@ + SecondaryDeviceKind kind = 1; + + //@@ .. cpp:var:: int64 device_id + //@@ + //@@ Identifier for the secondary device. + //@@ + int64 device_id = 2; + } + + //@@ .. cpp:var:: string name + //@@ + //@@ Optional name of this group of instances. If not specified the + //@@ name will be formed as _. The name of + //@@ individual instances will be further formed by a unique instance + //@@ number and GPU index: + //@@ + string name = 1; + + //@@ .. cpp:var:: Kind kind + //@@ + //@@ The kind of this instance group. Default is KIND_AUTO. If + //@@ KIND_AUTO or KIND_GPU then both 'count' and 'gpu' are valid and + //@@ may be specified. If KIND_CPU or KIND_MODEL only 'count' is valid + //@@ and 'gpu' cannot be specified. + //@@ + Kind kind = 4; + + //@@ .. cpp:var:: int32 count + //@@ + //@@ For a group assigned to GPU, the number of instances created for + //@@ each GPU listed in 'gpus'. For a group assigned to CPU the number + //@@ of instances created. Default is 1. + int32 count = 2; + + //@@ .. cpp:var:: ModelRateLimiter rate_limiter + //@@ + //@@ The rate limiter specific settings to be associated with this + //@@ instance group. Optional, if not specified no rate limiting + //@@ will be applied to this instance group. + //@@ + ModelRateLimiter rate_limiter = 6; + + //@@ .. cpp:var:: int32 gpus (repeated) + //@@ + //@@ GPU(s) where instances should be available. For each GPU listed, + //@@ 'count' instances of the model will be available. Setting 'gpus' + //@@ to empty (or not specifying at all) is equivalent to listing all + //@@ available GPUs. + //@@ + repeated int32 gpus = 3; + + //@@ .. cpp:var:: SecondaryDevice secondary_devices (repeated) + //@@ + //@@ Secondary devices that are required by instances specified by this + //@@ instance group. Optional. + //@@ + repeated SecondaryDevice secondary_devices = 8; + + //@@ .. cpp:var:: string profile (repeated) + //@@ + //@@ For TensorRT models containing multiple optimization profile, this + //@@ parameter specifies a set of optimization profiles available to this + //@@ instance group. The inference server will choose the optimal profile + //@@ based on the shapes of the input tensors. This field should lie + //@@ between 0 and - 1 + //@@ and be specified only for TensorRT backend, otherwise an error will + //@@ be generated. If not specified, the server will select the first + //@@ optimization profile by default. + //@@ + repeated string profile = 5; + + //@@ .. cpp:var:: bool passive + //@@ + //@@ Whether the instances within this instance group will be accepting + //@@ inference requests from the scheduler. If true, the instances will + //@@ not be added to the scheduler. Default value is false. + //@@ + bool passive = 7; + + //@@ .. cpp:var:: string host_policy + //@@ + //@@ The host policy name that the instance to be associated with. + //@@ The default value is set to reflect the device kind of the instance, + //@@ for instance, KIND_CPU is "cpu", KIND_MODEL is "model" and + //@@ KIND_GPU is "gpu_". + //@@ + string host_policy = 9; +} + +//@@ +//@@.. cpp:var:: message ModelTensorReshape +//@@ +//@@ Reshape specification for input and output tensors. +//@@ +message ModelTensorReshape +{ + //@@ .. cpp:var:: int64 shape (repeated) + //@@ + //@@ The shape to use for reshaping. + //@@ + repeated int64 shape = 1; +} + +//@@ +//@@.. cpp:var:: message ModelInput +//@@ +//@@ An input required by the model. +//@@ +message ModelInput +{ + //@@ + //@@ .. cpp:enum:: Format + //@@ + //@@ The format for the input. + //@@ + enum Format { + //@@ .. cpp:enumerator:: Format::FORMAT_NONE = 0 + //@@ + //@@ The input has no specific format. This is the default. + //@@ + FORMAT_NONE = 0; + + //@@ .. cpp:enumerator:: Format::FORMAT_NHWC = 1 + //@@ + //@@ HWC image format. Tensors with this format require 3 dimensions + //@@ if the model does not support batching (max_batch_size = 0) or 4 + //@@ dimensions if the model does support batching (max_batch_size + //@@ >= 1). In either case the 'dims' below should only specify the + //@@ 3 non-batch dimensions (i.e. HWC or CHW). + //@@ + FORMAT_NHWC = 1; + + //@@ .. cpp:enumerator:: Format::FORMAT_NCHW = 2 + //@@ + //@@ CHW image format. Tensors with this format require 3 dimensions + //@@ if the model does not support batching (max_batch_size = 0) or 4 + //@@ dimensions if the model does support batching (max_batch_size + //@@ >= 1). In either case the 'dims' below should only specify the + //@@ 3 non-batch dimensions (i.e. HWC or CHW). + //@@ + FORMAT_NCHW = 2; + } + + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the input. + //@@ + string name = 1; + + //@@ .. cpp:var:: DataType data_type + //@@ + //@@ The data-type of the input. + //@@ + DataType data_type = 2; + + //@@ .. cpp:var:: Format format + //@@ + //@@ The format of the input. Optional. + //@@ + Format format = 3; + + //@@ .. cpp:var:: int64 dims (repeated) + //@@ + //@@ The dimensions/shape of the input tensor that must be provided + //@@ when invoking the inference API for this model. + //@@ + repeated int64 dims = 4; + + //@@ .. cpp:var:: ModelTensorReshape reshape + //@@ + //@@ The shape expected for this input by the backend. The input will + //@@ be reshaped to this before being presented to the backend. The + //@@ reshape must have the same number of elements as the input shape + //@@ specified by 'dims'. Optional. + //@@ + ModelTensorReshape reshape = 5; + + //@@ .. cpp:var:: bool is_shape_tensor + //@@ + //@@ Whether or not the input is a shape tensor to the model. This field + //@@ is currently supported only for the TensorRT model. An error will be + //@@ generated if this specification does not comply with underlying + //@@ model. + //@@ + bool is_shape_tensor = 6; + + //@@ .. cpp:var:: bool allow_ragged_batch + //@@ + //@@ Whether or not the input is allowed to be "ragged" in a dynamically + //@@ created batch. Default is false indicating that two requests will + //@@ only be batched if this tensor has the same shape in both requests. + //@@ True indicates that two requests can be batched even if this tensor + //@@ has a different shape in each request. + //@@ + bool allow_ragged_batch = 7; + + //@@ .. cpp:var:: bool optional + //@@ + //@@ Whether or not the input is optional for the model execution. + //@@ If true, the input is not required in the inference request. + //@@ Default value is false. + //@@ + bool optional = 8; + + //@@ .. cpp:var:: bool is_non_linear_format_io + //@@ + //@@ Indicates whether the input tensor uses a non-linear IO format. This + //@@ field is currently supported only for TensorRT models. An error will + //@@ be generated if this specification does not comply with the + //@@ underlying model. + //@@ + bool is_non_linear_format_io = 9; +} + +//@@ +//@@.. cpp:var:: message ModelOutput +//@@ +//@@ An output produced by the model. +//@@ +message ModelOutput +{ + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the output. + //@@ + string name = 1; + + //@@ .. cpp:var:: DataType data_type + //@@ + //@@ The data-type of the output. + //@@ + DataType data_type = 2; + + //@@ .. cpp:var:: int64 dims (repeated) + //@@ + //@@ The dimensions/shape of the output tensor. + //@@ + repeated int64 dims = 3; + + //@@ .. cpp:var:: ModelTensorReshape reshape + //@@ + //@@ The shape produced for this output by the backend. The output will + //@@ be reshaped from this to the shape specified in 'dims' before being + //@@ returned in the inference response. The reshape must have the same + //@@ number of elements as the output shape specified by 'dims'. Optional. + //@@ + ModelTensorReshape reshape = 5; + + //@@ .. cpp:var:: string label_filename + //@@ + //@@ The label file associated with this output. Should be specified only + //@@ for outputs that represent classifications. Optional. + //@@ + string label_filename = 4; + + + //@@ .. cpp:var:: bool is_shape_tensor + //@@ + //@@ Whether or not the output is a shape tensor to the model. This field + //@@ is currently supported only for the TensorRT model. An error will be + //@@ generated if this specification does not comply with underlying + //@@ model. + //@@ + bool is_shape_tensor = 6; + + //@@ .. cpp:var:: bool is_non_linear_format_io + //@@ + //@@ Indicates whether the output tensor uses a non-linear IO format. This + //@@ field is currently supported only for TensorRT models. An error will + //@@ be generated if this specification does not comply with the + //@@ underlying model. + //@@ + bool is_non_linear_format_io = 7; +} + +//@@ .. cpp:var:: message BatchInput +//@@ +//@@ A batch input is an additional input that must be added by +//@@ the backend based on all the requests in a batch. +//@@ +message BatchInput +{ + //@@ + //@@ .. cpp:enum:: Kind + //@@ + //@@ The kind of the batch input. + //@@ + enum Kind { + //@@ .. cpp:enumerator:: Kind::BATCH_ELEMENT_COUNT = 0 + //@@ + //@@ The element count of the 'source_input' will be added as + //@@ input with shape [1]. + //@@ + BATCH_ELEMENT_COUNT = 0; + + //@@ .. cpp:enumerator:: Kind::BATCH_ACCUMULATED_ELEMENT_COUNT = 1 + //@@ + //@@ The accumulated element count of the 'source_input' will be + //@@ added as input with shape [1]. For example, if there is a + //@@ batch of two request, each with 2 elements, an input of value + //@@ 2 will be added to the first request, and an input of value + //@@ 4 will be added to the second request. + //@@ + BATCH_ACCUMULATED_ELEMENT_COUNT = 1; + + //@@ .. cpp:enumerator:: + //@@ Kind::BATCH_ACCUMULATED_ELEMENT_COUNT_WITH_ZERO = 2 + //@@ + //@@ The accumulated element count of the 'source_input' will be + //@@ added as input with shape [1], except for the first request + //@@ in the batch. For the first request in the batch, the input + //@@ will have shape [2] where the first element is value 0. + //@@ + BATCH_ACCUMULATED_ELEMENT_COUNT_WITH_ZERO = 2; + + //@@ .. cpp:enumerator:: Kind::BATCH_MAX_ELEMENT_COUNT_AS_SHAPE = 3 + //@@ + //@@ Among the requests in the batch, the max element count of the + //@@ 'source_input' will be added as input with shape + //@@ [max_element_count] for the first request in the batch. + //@@ For other requests, such input will be with shape [0]. + //@@ The data of the tensor will be uninitialized. + //@@ + BATCH_MAX_ELEMENT_COUNT_AS_SHAPE = 3; + + //@@ .. cpp:enumerator:: Kind::BATCH_ITEM_SHAPE = 4 + //@@ + //@@ Among the requests in the batch, the shape of the + //@@ 'source_input' will be added as input with shape + //@@ [batch_size, len(input_dim)]. For example, if one + //@@ batch-2 input with shape [3, 1] and batch-1 input + //@@ with shape [2, 2] are batched, the batch input will + //@@ have shape [3, 2] and value [ [3, 1], [3, 1], [2, 2]]. + //@@ + BATCH_ITEM_SHAPE = 4; + + //@@ .. cpp:enumerator:: Kind::BATCH_ITEM_SHAPE_FLATTEN = 5 + //@@ + //@@ Among the requests in the batch, the shape of the + //@@ 'source_input' will be added as input with single dimensional + //@@ shape [batch_size * len(input_dim)]. For example, if one + //@@ batch-2 input with shape [3, 1] and batch-1 input + //@@ with shape [2, 2] are batched, the batch input will + //@@ have shape [6] and value [3, 1, 3, 1, 2, 2]. + //@@ + BATCH_ITEM_SHAPE_FLATTEN = 5; + } + + //@@ .. cpp:var:: Kind kind + //@@ + //@@ The kind of this batch input. + //@@ + Kind kind = 1; + + //@@ .. cpp:var:: string target_name (repeated) + //@@ + //@@ The name of the model inputs that the backend will create + //@@ for this batch input. + //@@ + repeated string target_name = 2; + + //@@ .. cpp:var:: DataType data_type + //@@ + //@@ The input's datatype. The data type can be TYPE_INT32 or + //@@ TYPE_FP32. + //@@ + DataType data_type = 3; + + //@@ .. cpp:var:: string source_input (repeated) + //@@ + //@@ The backend derives the value for each batch input from one or + //@@ more other inputs. 'source_input' gives the names of those + //@@ inputs. + //@@ + repeated string source_input = 4; +} + +//@@.. cpp:var:: message BatchOutput +//@@ +//@@ A batch output is an output produced by the model that must be handled +//@@ differently by the backend based on all the requests in a batch. +//@@ +message BatchOutput +{ + //@@ + //@@ .. cpp:enum:: Kind + //@@ + //@@ The kind of the batch output. + //@@ + enum Kind { + //@@ .. cpp:enumerator:: Kind::BATCH_SCATTER_WITH_INPUT_SHAPE = 0 + //@@ + //@@ The output should be scattered according to the shape of + //@@ 'source_input'. The dynamic dimension of the output will + //@@ be set to the value of the same dimension in the input. + //@@ + BATCH_SCATTER_WITH_INPUT_SHAPE = 0; + } + + //@@ .. cpp:var:: string target_name (repeated) + //@@ + //@@ The name of the outputs to be produced by this batch output + //@@ specification. + //@@ + repeated string target_name = 1; + + //@@ .. cpp:var:: Kind kind + //@@ + //@@ The kind of this batch output. + //@@ + Kind kind = 2; + + //@@ .. cpp:var:: string source_input (repeated) + //@@ + //@@ The backend derives each batch output from one or more inputs. + //@@ 'source_input' gives the names of those inputs. + //@@ + repeated string source_input = 3; +} + +//@@ +//@@.. cpp:var:: message ModelVersionPolicy +//@@ +//@@ Policy indicating which versions of a model should be made +//@@ available by the inference server. +//@@ +message ModelVersionPolicy +{ + //@@ .. cpp:var:: message Latest + //@@ + //@@ Serve only the latest version(s) of a model. This is + //@@ the default policy. + //@@ + message Latest + { + //@@ .. cpp:var:: uint32 num_versions + //@@ + //@@ Serve only the 'num_versions' highest-numbered versions. T + //@@ The default value of 'num_versions' is 1, indicating that by + //@@ default only the single highest-number version of a + //@@ model will be served. + //@@ + uint32 num_versions = 1; + } + + //@@ .. cpp:var:: message All + //@@ + //@@ Serve all versions of the model. + //@@ + message All {} + + //@@ .. cpp:var:: message Specific + //@@ + //@@ Serve only specific versions of the model. + //@@ + message Specific + { + //@@ .. cpp:var:: int64 versions (repeated) + //@@ + //@@ The specific versions of the model that will be served. + //@@ + repeated int64 versions = 1; + } + + //@@ .. cpp:var:: oneof policy_choice + //@@ + //@@ Each model must implement only a single version policy. The + //@@ default policy is 'Latest'. + //@@ + oneof policy_choice + { + //@@ .. cpp:var:: Latest latest + //@@ + //@@ Serve only latest version(s) of the model. + //@@ + Latest latest = 1; + + //@@ .. cpp:var:: All all + //@@ + //@@ Serve all versions of the model. + //@@ + All all = 2; + + //@@ .. cpp:var:: Specific specific + //@@ + //@@ Serve only specific version(s) of the model. + //@@ + Specific specific = 3; + } +} + +//@@ +//@@.. cpp:var:: message ModelOptimizationPolicy +//@@ +//@@ Optimization settings for a model. These settings control if/how a +//@@ model is optimized and prioritized by the backend framework when +//@@ it is loaded. +//@@ +message ModelOptimizationPolicy +{ + //@@ + //@@ .. cpp:var:: message Graph + //@@ + //@@ Enable generic graph optimization of the model. If not specified + //@@ the framework's default level of optimization is used. Supports + //@@ TensorFlow graphdef and savedmodel and Onnx models. For TensorFlow + //@@ causes XLA to be enabled/disabled for the model. For Onnx defaults + //@@ to enabling all optimizations, -1 enables only basic optimizations, + //@@ +1 enables only basic and extended optimizations. + //@@ + message Graph + { + //@@ .. cpp:var:: int32 level + //@@ + //@@ The optimization level. Defaults to 0 (zero) if not specified. + //@@ + //@@ - -1: Disabled + //@@ - 0: Framework default + //@@ - 1+: Enable optimization level (greater values indicate + //@@ higher optimization levels) + //@@ + int32 level = 1; + } + + //@@ + //@@ .. cpp:enum:: ModelPriority + //@@ + //@@ Model priorities. A model will be given scheduling and execution + //@@ preference over models at lower priorities. Current model + //@@ priorities only work for TensorRT models. + //@@ + enum ModelPriority { + //@@ .. cpp:enumerator:: ModelPriority::PRIORITY_DEFAULT = 0 + //@@ + //@@ The default model priority. + //@@ + PRIORITY_DEFAULT = 0; + + //@@ .. cpp:enumerator:: ModelPriority::PRIORITY_MAX = 1 + //@@ + //@@ The maximum model priority. + //@@ + PRIORITY_MAX = 1; + + //@@ .. cpp:enumerator:: ModelPriority::PRIORITY_MIN = 2 + //@@ + //@@ The minimum model priority. + //@@ + PRIORITY_MIN = 2; + } + + //@@ + //@@ .. cpp:var:: message Cuda + //@@ + //@@ CUDA-specific optimization settings. + //@@ + message Cuda + { + //@@ .. cpp:var:: message GraphSpec + //@@ + //@@ Specification of the CUDA graph to be captured. + //@@ + message GraphSpec + { + //@@ .. cpp:var:: message Dims + //@@ + //@@ Specification of tensor dimension. + //@@ + message Shape + { + //@@ .. cpp:var:: int64 dim (repeated) + //@@ + //@@ The dimension. + //@@ + repeated int64 dim = 1; + } + + message LowerBound + { + //@@ .. cpp:var:: int32 batch_size + //@@ + //@@ The batch size of the CUDA graph. If 'max_batch_size' is 0, + //@@ 'batch_size' must be set to 0. Otherwise, 'batch_size' must + //@@ be set to value between 1 and 'max_batch_size'. + //@@ + int32 batch_size = 1; + + //@@ .. cpp:var:: map input + //@@ + //@@ The specification of the inputs. 'Shape' is the shape of + //@@ the input without batching dimension. + //@@ + map input = 2; + } + + //@@ .. cpp:var:: int32 batch_size + //@@ + //@@ The batch size of the CUDA graph. If 'max_batch_size' is 0, + //@@ 'batch_size' must be set to 0. Otherwise, 'batch_size' must + //@@ be set to value between 1 and 'max_batch_size'. + //@@ + int32 batch_size = 1; + + //@@ .. cpp:var:: map input + //@@ + //@@ The specification of the inputs. 'Shape' is the shape of the + //@@ input without batching dimension. + //@@ + map input = 2; + + //@@ .. cpp:var:: LowerBound graph_lower_bound + //@@ + //@@ Specify the lower bound of the CUDA graph. Optional. + //@@ If specified, the graph can be used for input shapes and + //@@ batch sizes that are in closed interval between the lower + //@@ bound specification and graph specification. For dynamic + //@@ shape model, this allows CUDA graphs to be launched + //@@ frequently without capturing all possible shape combinations. + //@@ However, using graph for shape combinations different from + //@@ the one used for capturing introduces uninitialized data for + //@@ execution and it may distort the inference result if + //@@ the model is sensitive to uninitialized data. + //@@ + LowerBound graph_lower_bound = 3; + } + + //@@ .. cpp:var:: bool graphs + //@@ + //@@ Use CUDA graphs API to capture model operations and execute + //@@ them more efficiently. Default value is false. + //@@ Currently only recognized by TensorRT backend. + //@@ + bool graphs = 1; + + //@@ .. cpp:var:: bool busy_wait_events + //@@ + //@@ Use busy-waiting to synchronize CUDA events to achieve minimum + //@@ latency from event complete to host thread to be notified, with + //@@ the cost of high CPU load. Default value is false. + //@@ Currently only recognized by TensorRT backend. + //@@ + bool busy_wait_events = 2; + + //@@ .. cpp:var:: GraphSpec graph_spec (repeated) + //@@ + //@@ Specification of the CUDA graph to be captured. If not specified + //@@ and 'graphs' is true, the default CUDA graphs will be captured + //@@ based on model settings. + //@@ Currently only recognized by TensorRT backend. + //@@ + repeated GraphSpec graph_spec = 3; + + //@@ .. cpp:var:: bool output_copy_stream + //@@ + //@@ Uses a CUDA stream separate from the inference stream to copy the + //@@ output to host. However, be aware that setting this option to + //@@ true will lead to an increase in the memory consumption of the + //@@ model as Triton will allocate twice as much GPU memory for its + //@@ I/O tensor buffers. Default value is false. + //@@ Currently only recognized by TensorRT backend. + //@@ + bool output_copy_stream = 4; + } + + //@@ + //@@ .. cpp:var:: message ExecutionAccelerators + //@@ + //@@ Specify the preferred execution accelerators to be used to execute + //@@ the model. Currently only recognized by ONNX Runtime backend and + //@@ TensorFlow backend. + //@@ + //@@ For ONNX Runtime backend, it will deploy the model with the execution + //@@ accelerators by priority, the priority is determined based on the + //@@ order that they are set, i.e. the provider at the front has highest + //@@ priority. Overall, the priority will be in the following order: + //@@ (if instance is on GPU) + //@@ CUDA Execution Provider (if instance is on GPU) + //@@ + //@@ Default CPU Execution Provider + //@@ + message ExecutionAccelerators + { + //@@ + //@@ .. cpp:var:: message Accelerator + //@@ + //@@ Specify the accelerator to be used to execute the model. + //@@ Accelerator with the same name may accept different parameters + //@@ depending on the backends. + //@@ + message Accelerator + { + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the execution accelerator. + //@@ + string name = 1; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ Additional parameters used to configure the accelerator. + //@@ + map parameters = 2; + } + + //@@ .. cpp:var:: Accelerator gpu_execution_accelerator (repeated) + //@@ + //@@ The preferred execution provider to be used if the model instance + //@@ is deployed on GPU. + //@@ + //@@ For ONNX Runtime backend, possible value is "tensorrt" as name, + //@@ and no parameters are required. + //@@ + //@@ For TensorFlow backend, possible values are "tensorrt", + //@@ "auto_mixed_precision", "gpu_io". + //@@ + //@@ For "tensorrt", the following parameters can be specified: + //@@ "precision_mode": The precision used for optimization. + //@@ Allowed values are "FP32" and "FP16". Default value is "FP32". + //@@ + //@@ "max_cached_engines": The maximum number of cached TensorRT + //@@ engines in dynamic TensorRT ops. Default value is 100. + //@@ + //@@ "minimum_segment_size": The smallest model subgraph that will + //@@ be considered for optimization by TensorRT. Default value is 3. + //@@ + //@@ "max_workspace_size_bytes": The maximum GPU memory the model + //@@ can use temporarily during execution. Default value is 1GB. + //@@ + //@@ For "auto_mixed_precision", no parameters are required. If set, + //@@ the model will try to use FP16 for better performance. + //@@ This optimization can not be set with "tensorrt". + //@@ + //@@ For "gpu_io", no parameters are required. If set, the model will + //@@ be executed using TensorFlow Callable API to set input and output + //@@ tensors in GPU memory if possible, which can reduce data transfer + //@@ overhead if the model is used in ensemble. However, the Callable + //@@ object will be created on model creation and it will request all + //@@ outputs for every model execution, which may impact the + //@@ performance if a request does not require all outputs. This + //@@ optimization will only take affect if the model instance is + //@@ created with KIND_GPU. + //@@ + repeated Accelerator gpu_execution_accelerator = 1; + + //@@ .. cpp:var:: Accelerator cpu_execution_accelerator (repeated) + //@@ + //@@ The preferred execution provider to be used if the model instance + //@@ is deployed on CPU. + //@@ + //@@ For ONNX Runtime backend, possible value is "openvino" as name, + //@@ and no parameters are required. + //@@ + repeated Accelerator cpu_execution_accelerator = 2; + } + + //@@ + //@@ .. cpp:var:: message PinnedMemoryBuffer + //@@ + //@@ Specify whether to use a pinned memory buffer when transferring data + //@@ between non-pinned system memory and GPU memory. Using a pinned + //@@ memory buffer for system from/to GPU transfers will typically provide + //@@ increased performance. For example, in the common use case where the + //@@ request provides inputs and delivers outputs via non-pinned system + //@@ memory, if the model instance accepts GPU IOs, the inputs will be + //@@ processed by two copies: from non-pinned system memory to pinned + //@@ memory, and from pinned memory to GPU memory. Similarly, pinned + //@@ memory will be used for delivering the outputs. + //@@ + message PinnedMemoryBuffer + { + //@@ .. cpp:var:: bool enable + //@@ + //@@ Use pinned memory buffer. Default is true. + //@@ + bool enable = 1; + } + + //@@ .. cpp:var:: Graph graph + //@@ + //@@ The graph optimization setting for the model. Optional. + //@@ + Graph graph = 1; + + //@@ .. cpp:var:: ModelPriority priority + //@@ + //@@ The priority setting for the model. Optional. + //@@ + ModelPriority priority = 2; + + //@@ .. cpp:var:: Cuda cuda + //@@ + //@@ CUDA-specific optimization settings. Optional. + //@@ + Cuda cuda = 3; + + //@@ .. cpp:var:: ExecutionAccelerators execution_accelerators + //@@ + //@@ The accelerators used for the model. Optional. + //@@ + ExecutionAccelerators execution_accelerators = 4; + + //@@ .. cpp:var:: PinnedMemoryBuffer input_pinned_memory + //@@ + //@@ Use pinned memory buffer when the data transfer for inputs + //@@ is between GPU memory and non-pinned system memory. + //@@ Default is true. + //@@ + PinnedMemoryBuffer input_pinned_memory = 5; + + //@@ .. cpp:var:: PinnedMemoryBuffer output_pinned_memory + //@@ + //@@ Use pinned memory buffer when the data transfer for outputs + //@@ is between GPU memory and non-pinned system memory. + //@@ Default is true. + //@@ + PinnedMemoryBuffer output_pinned_memory = 6; + + //@@ .. cpp:var:: uint32 gather_kernel_buffer_threshold + //@@ + //@@ The backend may use a gather kernel to gather input data if the + //@@ device has direct access to the source buffer and the destination + //@@ buffer. In such case, the gather kernel will be used only if the + //@@ number of buffers to be gathered is greater or equal to + //@@ the specified value. If 0, the gather kernel will be disabled. + //@@ Default value is 0. + //@@ Currently only recognized by TensorRT backend. + //@@ + uint32 gather_kernel_buffer_threshold = 7; + + //@@ .. cpp:var:: bool eager_batching + //@@ + //@@ Start preparing the next batch before the model instance is ready + //@@ for the next inference. This option can be used to overlap the + //@@ batch preparation with model execution, with the trade-off that + //@@ the next batch might be smaller than what it could have been. + //@@ Default value is false. + //@@ Currently only recognized by TensorRT backend. + //@@ + bool eager_batching = 8; +} + +//@@ +//@@.. cpp:var:: message ModelQueuePolicy +//@@ +//@@ Queue policy for inference requests. +//@@ +message ModelQueuePolicy +{ + //@@ + //@@ .. cpp:enum:: TimeoutAction + //@@ + //@@ The action applied to timed-out requests. + //@@ + enum TimeoutAction { + //@@ .. cpp:enumerator:: Action::REJECT = 0 + //@@ + //@@ Reject the request and return error message accordingly. + //@@ + REJECT = 0; + + //@@ .. cpp:enumerator:: Action::DELAY = 1 + //@@ + //@@ Delay the request until all other requests at the same + //@@ (or higher) priority levels that have not reached their timeouts + //@@ are processed. A delayed request will eventually be processed, + //@@ but may be delayed indefinitely due to newly arriving requests. + //@@ + DELAY = 1; + } + + //@@ + //@@ .. cpp:var:: TimeoutAction timeout_action + //@@ + //@@ The action applied to timed-out request. + //@@ The default action is REJECT. + //@@ + TimeoutAction timeout_action = 1; + + //@@ + //@@ .. cpp:var:: uint64 default_timeout_microseconds + //@@ + //@@ The default timeout for every request, in microseconds. + //@@ The default value is 0 which indicates that no timeout is set. + //@@ + uint64 default_timeout_microseconds = 2; + + //@@ + //@@ .. cpp:var:: bool allow_timeout_override + //@@ + //@@ Whether individual request can override the default timeout value. + //@@ When true, individual requests can set a timeout that is less than + //@@ the default timeout value but may not increase the timeout. + //@@ The default value is false. + //@@ + bool allow_timeout_override = 3; + + //@@ + //@@ .. cpp:var:: uint32 max_queue_size + //@@ + //@@ The maximum queue size for holding requests. A request will be + //@@ rejected immediately if it can't be enqueued because the queue is + //@@ full. The default value is 0 which indicates that no maximum + //@@ queue size is enforced. + //@@ + uint32 max_queue_size = 4; +} + +//@@ +//@@.. cpp:var:: message ModelDynamicBatching +//@@ +//@@ Dynamic batching configuration. These settings control how dynamic +//@@ batching operates for the model. +//@@ +message ModelDynamicBatching +{ + //@@ .. cpp:var:: int32 preferred_batch_size (repeated) + //@@ + //@@ Preferred batch sizes for dynamic batching. If a batch of one of + //@@ these sizes can be formed it will be executed immediately. If + //@@ not specified a preferred batch size will be chosen automatically + //@@ based on model and GPU characteristics. + //@@ + repeated int32 preferred_batch_size = 1; + + //@@ .. cpp:var:: uint64 max_queue_delay_microseconds + //@@ + //@@ The maximum time, in microseconds, a request will be delayed in + //@@ the scheduling queue to wait for additional requests for + //@@ batching. Default is 0. + //@@ + uint64 max_queue_delay_microseconds = 2; + + //@@ .. cpp:var:: bool preserve_ordering + //@@ + //@@ Should the dynamic batcher preserve the ordering of responses to + //@@ match the order of requests received by the scheduler. Default is + //@@ false. If true, the responses will be returned in the same order as + //@@ the order of requests sent to the scheduler. If false, the responses + //@@ may be returned in arbitrary order. This option is specifically + //@@ needed when a sequence of related inference requests (i.e. inference + //@@ requests with the same correlation ID) are sent to the dynamic + //@@ batcher to ensure that the sequence responses are in the correct + //@@ order. + //@@ + bool preserve_ordering = 3; + + //@@ .. cpp:var:: uint64 priority_levels + //@@ + //@@ The number of priority levels to be enabled for the model, + //@@ the priority level starts from 1 and 1 is the highest priority. + //@@ Requests are handled in priority order with all priority 1 requests + //@@ processed before priority 2, all priority 2 requests processed before + //@@ priority 3, etc. Requests with the same priority level will be + //@@ handled in the order that they are received. + //@@ + uint64 priority_levels = 4; + + //@@ .. cpp:var:: uint64 default_priority_level + //@@ + //@@ The priority level used for requests that don't specify their + //@@ priority. The value must be in the range [ 1, 'priority_levels' ]. + //@@ + uint64 default_priority_level = 5; + + //@@ .. cpp:var:: ModelQueuePolicy default_queue_policy + //@@ + //@@ The default queue policy used for requests that don't require + //@@ priority handling and requests that specify priority levels where + //@@ there is no specific policy given. If not specified, a policy with + //@@ default field values will be used. + //@@ + ModelQueuePolicy default_queue_policy = 6; + + //@@ .. cpp:var:: map priority_queue_policy + //@@ + //@@ Specify the queue policy for the priority level. The default queue + //@@ policy will be used if a priority level doesn't specify a queue + //@@ policy. + //@@ + map priority_queue_policy = 7; +} + +//@@ +//@@.. cpp:var:: message ModelSequenceBatching +//@@ +//@@ Sequence batching configuration. These settings control how sequence +//@@ batching operates for the model. +//@@ +message ModelSequenceBatching +{ + //@@ .. cpp:var:: message Control + //@@ + //@@ A control is a signal that the sequence batcher uses to + //@@ communicate with a backend. + //@@ + message Control + { + //@@ + //@@ .. cpp:enum:: Kind + //@@ + //@@ The kind of the control. + //@@ + enum Kind { + //@@ .. cpp:enumerator:: Kind::CONTROL_SEQUENCE_START = 0 + //@@ + //@@ A new sequence is/is-not starting. If true a sequence is + //@@ starting, if false a sequence is continuing. Must + //@@ specify either int32_false_true, fp32_false_true or + //@@ bool_false_true for this control. This control is optional. + //@@ + CONTROL_SEQUENCE_START = 0; + + //@@ .. cpp:enumerator:: Kind::CONTROL_SEQUENCE_READY = 1 + //@@ + //@@ A sequence is/is-not ready for inference. If true the + //@@ input tensor data is valid and should be used. If false + //@@ the input tensor data is invalid and inferencing should + //@@ be "skipped". Must specify either int32_false_true, + //@@ fp32_false_true or bool_false_true for this control. This + //@@ control is optional. + //@@ + CONTROL_SEQUENCE_READY = 1; + + //@@ .. cpp:enumerator:: Kind::CONTROL_SEQUENCE_END = 2 + //@@ + //@@ A sequence is/is-not ending. If true a sequence is + //@@ ending, if false a sequence is continuing. Must specify + //@@ either int32_false_true, fp32_false_true or bool_false_true + //@@ for this control. This control is optional. + //@@ + CONTROL_SEQUENCE_END = 2; + + //@@ .. cpp:enumerator:: Kind::CONTROL_SEQUENCE_CORRID = 3 + //@@ + //@@ The correlation ID of the sequence. The correlation ID + //@@ is an uint64_t value that is communicated in whole or + //@@ in part by the tensor. The tensor's datatype must be + //@@ specified by data_type and must be TYPE_UINT64, TYPE_INT64, + //@@ TYPE_UINT32 or TYPE_INT32. If a 32-bit datatype is specified + //@@ the correlation ID will be truncated to the low-order 32 + //@@ bits. This control is optional. + //@@ + CONTROL_SEQUENCE_CORRID = 3; + } + + //@@ .. cpp:var:: Kind kind + //@@ + //@@ The kind of this control. + //@@ + Kind kind = 1; + + //@@ .. cpp:var:: int32 int32_false_true (repeated) + //@@ + //@@ The control's true and false setting is indicated by setting + //@@ a value in an int32 tensor. The tensor must be a + //@@ 1-dimensional tensor with size equal to the batch size of + //@@ the request. 'int32_false_true' must have two entries: the + //@@ first the false value and the second the true value. + //@@ + repeated int32 int32_false_true = 2; + + //@@ .. cpp:var:: float fp32_false_true (repeated) + //@@ + //@@ The control's true and false setting is indicated by setting + //@@ a value in a fp32 tensor. The tensor must be a + //@@ 1-dimensional tensor with size equal to the batch size of + //@@ the request. 'fp32_false_true' must have two entries: the + //@@ first the false value and the second the true value. + //@@ + repeated float fp32_false_true = 3; + + //@@ .. cpp:var:: bool bool_false_true (repeated) + //@@ + //@@ The control's true and false setting is indicated by setting + //@@ a value in a bool tensor. The tensor must be a + //@@ 1-dimensional tensor with size equal to the batch size of + //@@ the request. 'bool_false_true' must have two entries: the + //@@ first the false value and the second the true value. + //@@ + repeated bool bool_false_true = 5; + + //@@ .. cpp:var:: DataType data_type + //@@ + //@@ The control's datatype. + //@@ + DataType data_type = 4; + } + + //@@ .. cpp:var:: message ControlInput + //@@ + //@@ The sequence control values to communicate by a model input. + //@@ + message ControlInput + { + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the model input. + //@@ + string name = 1; + + //@@ .. cpp:var:: Control control (repeated) + //@@ + //@@ The control value(s) that should be communicated to the + //@@ model using this model input. + //@@ + repeated Control control = 2; + } + + //@@ + //@@ .. cpp:var:: message InitialState + //@@ + //@@ Settings used to initialize data for implicit state. + //@@ + message InitialState + { + //@@ .. cpp:var:: DataType data_type + //@@ + //@@ The data-type of the state. + //@@ + DataType data_type = 1; + + //@@ .. cpp:var:: int64 dims (repeated) + //@@ + //@@ The shape of the state tensor, not including the batch + //@@ dimension. + //@@ + repeated int64 dims = 2; + + //@@ .. cpp:var:: oneof state_data + //@@ + //@@ Specify how the initial state data is generated. + //@@ + oneof state_data + { + //@@ + //@@ .. cpp:var:: bool zero_data + //@@ + //@@ The identifier for using zeros as initial state data. + //@@ Note that the value of 'zero_data' will not be checked, + //@@ instead, zero data will be used as long as the field is set. + //@@ + bool zero_data = 3; + + //@@ .. cpp:var:: string data_file + //@@ + //@@ The file whose content will be used as the initial data for + //@@ the state in row-major order. The file must be provided in + //@@ sub-directory 'initial_state' under the model directory. + //@@ + string data_file = 4; + } + + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the state initialization. + //@@ + string name = 5; + } + + //@@ .. cpp:var:: message State + //@@ + //@@ An input / output pair of tensors that carry state for the sequence. + //@@ + message State + { + //@@ .. cpp:var:: string input_name + //@@ + //@@ The name of the model state input. + //@@ + string input_name = 1; + + //@@ .. cpp:var:: string output_name + //@@ + //@@ The name of the model state output. + //@@ + string output_name = 2; + + //@@ .. cpp:var:: DataType data_type + //@@ + //@@ The data-type of the state. + //@@ + DataType data_type = 3; + + //@@ .. cpp:var:: int64 dim (repeated) + //@@ + //@@ The dimension. + //@@ + repeated int64 dims = 4; + + //@@ .. cpp:var:: InitialState initial_state (repeated) + //@@ + //@@ The optional field to specify the initial state for the model. + //@@ + repeated InitialState initial_state = 5; + + //@@ .. cpp:var:: bool use_same_buffer_for_input_output + //@@ + //@@ The optional field to use a single buffer for both input and output + //@@ state. Without this option, Triton allocates separate buffers + //@@ for input and output state + //@@ which can be problematic if the state size is + //@@ large. This option reduces the memory usage by allocating a single + //@@ buffer. Enabling this option is recommended whenever + //@@ the input state is processed before the output state is written. + //@@ When enabled the state + //@@ will always be updated independent of whether + //@@ TRITONBACKEND_StateUpdate is called + //@@ (however TRITONBACKEND_StateUpdate should still be called for + //@@ completeness). + //@@ + //@@ The default value is false. + //@@ + bool use_same_buffer_for_input_output = 6; + + //@@ .. cpp:var:: bool use_growable_memory + //@@ + //@@ The optional field to enable an implicit state buffer to grow + //@@ without reallocating or copying existing memory. + //@@ Additional memory will be appended to the end of the buffer and + //@@ existing data will be preserved. + //@@ This option is only available for CUDA memory and requires enabling + //@@ use_same_buffer_for_input_output. When using this option, + //@@ StateBuffer call will always return CUDA memory even if CPU memory + //@@ is requested. + //@@ + //@@ The default value is false. + //@@ + bool use_growable_memory = 7; + } + + //@@ .. cpp:var:: message StrategyDirect + //@@ + //@@ The sequence batcher uses a specific, unique batch + //@@ slot for each sequence. All inference requests in a + //@@ sequence are directed to the same batch slot in the same + //@@ model instance over the lifetime of the sequence. This + //@@ is the default strategy. + //@@ + message StrategyDirect + { + //@@ .. cpp:var:: uint64 max_queue_delay_microseconds + //@@ + //@@ The maximum time, in microseconds, a candidate request + //@@ will be delayed in the sequence batch scheduling queue to + //@@ wait for additional requests for batching. Default is 0. + //@@ + uint64 max_queue_delay_microseconds = 1; + + //@@ .. cpp:var:: float minimum_slot_utilization + //@@ + //@@ The minimum slot utilization that must be satisfied to + //@@ execute the batch before 'max_queue_delay_microseconds' expires. + //@@ For example, a value of 0.5 indicates that the batch should be + //@@ executed as soon as 50% or more of the slots are ready even if + //@@ the 'max_queue_delay_microseconds' timeout has not expired. + //@@ The default is 0.0, indicating that a batch will be executed + //@@ before 'max_queue_delay_microseconds' timeout expires if at least + //@@ one batch slot is ready. 'max_queue_delay_microseconds' will be + //@@ ignored unless minimum_slot_utilization is set to a non-zero + //@@ value. + //@@ + float minimum_slot_utilization = 2; + } + + //@@ .. cpp:var:: message StrategyOldest + //@@ + //@@ The sequence batcher maintains up to 'max_candidate_sequences' + //@@ candidate sequences. 'max_candidate_sequences' can be greater + //@@ than the model's 'max_batch_size'. For inferencing the batcher + //@@ chooses from the candidate sequences up to 'max_batch_size' + //@@ inference requests. Requests are chosen in an oldest-first + //@@ manner across all candidate sequences. A given sequence is + //@@ not guaranteed to be assigned to the same batch slot for + //@@ all inference requests of that sequence. + //@@ + message StrategyOldest + { + //@@ .. cpp:var:: int32 max_candidate_sequences + //@@ + //@@ Maximum number of candidate sequences that the batcher + //@@ maintains. Excess sequences are kept in an ordered backlog + //@@ and become candidates when existing candidate sequences + //@@ complete. + //@@ + int32 max_candidate_sequences = 1; + + //@@ .. cpp:var:: int32 preferred_batch_size (repeated) + //@@ + //@@ Preferred batch sizes for dynamic batching of candidate + //@@ sequences. If a batch of one of these sizes can be formed + //@@ it will be executed immediately. If not specified a + //@@ preferred batch size will be chosen automatically + //@@ based on model and GPU characteristics. + //@@ + repeated int32 preferred_batch_size = 2; + + //@@ .. cpp:var:: uint64 max_queue_delay_microseconds + //@@ + //@@ The maximum time, in microseconds, a candidate request + //@@ will be delayed in the dynamic batch scheduling queue to + //@@ wait for additional requests for batching. Default is 0. + //@@ + uint64 max_queue_delay_microseconds = 3; + + //@@ .. cpp:var:: bool preserve_ordering + //@@ + //@@ Should the dynamic batcher preserve the ordering of responses to + //@@ match the order of requests received by the scheduler. Default is + //@@ false. If true, the responses will be returned in the same order + //@@ as the order of requests sent to the scheduler. If false, the + //@@ responses may be returned in arbitrary order. This option is + //@@ specifically needed when a sequence of related inference requests + //@@ (i.e. inference requests with the same correlation ID) are sent + //@@ to the dynamic batcher to ensure that the sequence responses are + //@@ in the correct order. + //@@ + //@@ When using decoupled models, setting this to true may block the + //@@ responses from independent sequences from being returned to the + //@@ client until the previous request completes, hurting overall + //@@ performance. If using GRPC streaming protocol, the stream + //@@ ordering guarantee may be sufficient alone to ensure the + //@@ responses for each sequence are returned in sequence-order + //@@ without blocking based on independent requests, depending on the + //@@ use case. + //@@ + bool preserve_ordering = 4; + } + + //@@ .. cpp:var:: oneof strategy_choice + //@@ + //@@ The strategy used by the sequence batcher. Default strategy + //@@ is 'direct'. + //@@ + oneof strategy_choice + { + //@@ .. cpp:var:: StrategyDirect direct + //@@ + //@@ StrategyDirect scheduling strategy. + //@@ + StrategyDirect direct = 3; + + //@@ .. cpp:var:: StrategyOldest oldest + //@@ + //@@ StrategyOldest scheduling strategy. + //@@ + StrategyOldest oldest = 4; + } + + //@@ .. cpp:var:: uint64 max_sequence_idle_microseconds + //@@ + //@@ The maximum time, in microseconds, that a sequence is allowed to + //@@ be idle before it is aborted. The inference server considers a + //@@ sequence idle when it does not have any inference request queued + //@@ for the sequence. If this limit is exceeded, the inference server + //@@ will free the sequence slot allocated by the sequence and make it + //@@ available for another sequence. If not specified (or specified as + //@@ zero) a default value of 1000000 (1 second) is used. + //@@ + uint64 max_sequence_idle_microseconds = 1; + + //@@ .. cpp:var:: ControlInput control_input (repeated) + //@@ + //@@ The model input(s) that the server should use to communicate + //@@ sequence start, stop, ready and similar control values to the + //@@ model. + //@@ + repeated ControlInput control_input = 2; + + //@@ .. cpp:var:: State state (repeated) + //@@ + //@@ The optional state that can be stored in Triton for performing + //@@ inference requests on a sequence. Each sequence holds an implicit + //@@ state local to itself. The output state tensor provided by the + //@@ model in 'output_name' field of the current inference request will + //@@ be transferred as an input tensor named 'input_name' in the next + //@@ request of the same sequence. The input state of the first request + //@@ in the sequence contains garbage data. + //@@ + repeated State state = 5; + + //@@ .. cpp:var:: bool iterative_sequence + //@@ + //@@ Requests for iterative sequences are processed over a number + //@@ of iterations. An iterative sequence is initiated by a single + //@@ request and is "rescheduled" by the model until completion. + //@@ Requests for inflight requests will be batched together + //@@ and can complete independently. Note this feature + //@@ requires backend support. Default value is false. + bool iterative_sequence = 6; +} + +//@@ +//@@.. cpp:var:: message ModelEnsembling +//@@ +//@@ Model ensembling configuration. These settings specify the models that +//@@ compose the ensemble and how data flows between the models. +//@@ +message ModelEnsembling +{ + //@@ .. cpp:var:: message Step + //@@ + //@@ Each step specifies a model included in the ensemble, + //@@ maps ensemble tensor names to the model input tensors, + //@@ and maps model output tensors to ensemble tensor names + //@@ + message Step + { + //@@ .. cpp:var:: string model_name + //@@ + //@@ The name of the model to execute for this step of the ensemble. + //@@ + string model_name = 1; + + //@@ .. cpp:var:: int64 model_version + //@@ + //@@ The version of the model to use for inference. If -1 + //@@ the latest/most-recent version of the model is used. + //@@ + int64 model_version = 2; + + //@@ .. cpp:var:: map input_map + //@@ + //@@ Map from name of an input tensor on this step's model to ensemble + //@@ tensor name. The ensemble tensor must have the same data type and + //@@ shape as the model input. Each model input must be assigned to + //@@ one ensemble tensor, but the same ensemble tensor can be assigned + //@@ to multiple model inputs. + //@@ + map input_map = 3; + + //@@ .. cpp:var:: map output_map + //@@ + //@@ Map from name of an output tensor on this step's model to ensemble + //@@ tensor name. The data type and shape of the ensemble tensor will + //@@ be inferred from the model output. It is optional to assign all + //@@ model outputs to ensemble tensors. One ensemble tensor name + //@@ can appear in an output map only once. + //@@ + map output_map = 4; + + //@@ .. cpp:var:: string model_namespace + //@@ + //@@ [RESERVED] currently this field is reserved for internal use, users + //@@ must not set any value to this field to avoid unexpected behavior. + //@@ + string model_namespace = 5; + } + + //@@ .. cpp:var:: Step step (repeated) + //@@ + //@@ The models and the input / output mappings used within the ensemble. + //@@ + repeated Step step = 1; +} + +//@@ +//@@.. cpp:var:: message ModelParameter +//@@ +//@@ A model parameter. +//@@ +message ModelParameter +{ + //@@ .. cpp:var:: string string_value + //@@ + //@@ The string value of the parameter. + //@@ + string string_value = 1; +} + +//@@ +//@@.. cpp:var:: message ModelWarmup +//@@ +//@@ Settings used to construct the request sample for model warmup. +//@@ +message ModelWarmup +{ + //@@ + //@@ .. cpp:var:: message Input + //@@ + //@@ Meta data associated with an input. + //@@ + message Input + { + //@@ .. cpp:var:: DataType data_type + //@@ + //@@ The data-type of the input. + //@@ + DataType data_type = 1; + + //@@ .. cpp:var:: int64 dims (repeated) + //@@ + //@@ The shape of the input tensor, not including the batch dimension. + //@@ + repeated int64 dims = 2; + + //@@ .. cpp:var:: oneof input_data_type + //@@ + //@@ Specify how the input data is generated. If the input has STRING + //@@ data type and 'random_data' is set, the data generation will fall + //@@ back to 'zero_data'. + //@@ + oneof input_data_type + { + //@@ + //@@ .. cpp:var:: bool zero_data + //@@ + //@@ The identifier for using zeros as input data. Note that the + //@@ value of 'zero_data' will not be checked, instead, zero data + //@@ will be used as long as the field is set. + //@@ + bool zero_data = 3; + + //@@ + //@@ .. cpp:var:: bool random_data + //@@ + //@@ The identifier for using random data as input data. Note that + //@@ the value of 'random_data' will not be checked, instead, + //@@ random data will be used as long as the field is set. + //@@ + bool random_data = 4; + + //@@ .. cpp:var:: string input_data_file + //@@ + //@@ The file whose content will be used as raw input data in + //@@ row-major order. The file must be provided in a sub-directory + //@@ 'warmup' under the model directory. The file contents should be + //@@ in binary format. For TYPE_STRING data-type, an element is + //@@ represented by a 4-byte unsigned integer giving the length + //@@ followed by the actual bytes. + //@@ + string input_data_file = 5; + } + } + + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the request sample. + //@@ + string name = 1; + + //@@ .. cpp:var:: uint32 batch_size + //@@ + //@@ The batch size of the inference request. This must be >= 1. For + //@@ models that don't support batching, batch_size must be 1. If + //@@ batch_size > 1, the 'inputs' specified below will be duplicated to + //@@ match the batch size requested. + //@@ + uint32 batch_size = 2; + + //@@ .. cpp:var:: map inputs + //@@ + //@@ The warmup meta data associated with every model input, including + //@@ control tensors. + //@@ + map inputs = 3; + + //@@ .. cpp:var:: uint32 count + //@@ + //@@ The number of iterations that this warmup sample will be executed. + //@@ For example, if this field is set to 2, 2 model executions using this + //@@ sample will be scheduled for warmup. Default value is 0 which + //@@ indicates that this sample will be used only once. + //@@ Note that for sequence model, 'count' may not work well + //@@ because the model often expect a valid sequence of requests which + //@@ should be represented by a series of warmup samples. 'count > 1' + //@@ essentially "resends" one of the sample, which may invalidate the + //@@ sequence and result in unexpected warmup failure. + //@@ + uint32 count = 4; +} + +//@@ +//@@ .. cpp:var:: message ModelOperations +//@@ +//@@ The metadata of libraries providing custom operations for this model. +//@@ +message ModelOperations +{ + //@@ .. cpp:var:: string op_library_filename (repeated) + //@@ + //@@ Optional paths of the libraries providing custom operations for + //@@ this model. Valid only for ONNX models. + //@@ + repeated string op_library_filename = 1; +} + +//@@ +//@@ .. cpp:var:: message ModelTransactionPolicy +//@@ +//@@ The specification that describes the nature of transactions +//@@ to be expected from the model. +//@@ +message ModelTransactionPolicy +{ + //@@ .. cpp:var:: bool decoupled + //@@ + //@@ Indicates whether responses generated by the model are decoupled with + //@@ the requests issued to it, which means the number of responses + //@@ generated by model may differ from number of requests issued, and + //@@ that the responses may be out of order relative to the order of + //@@ requests. The default is false, which means the model will generate + //@@ exactly one response for each request. + //@@ + bool decoupled = 1; +} + +//@@ +//@@.. cpp:var:: message ModelRepositoryAgents +//@@ +//@@ The repository agents for the model. +//@@ +message ModelRepositoryAgents +{ + //@@ + //@@ .. cpp:var:: message Agent + //@@ + //@@ A repository agent that should be invoked for the specified + //@@ repository actions for this model. + //@@ + message Agent + { + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the agent. + //@@ + string name = 1; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ The parameters for the agent. + //@@ + map parameters = 2; + } + + //@@ + //@@ .. cpp:var:: Agent agents (repeated) + //@@ + //@@ The ordered list of agents for the model. These agents will be + //@@ invoked in order to respond to repository actions occurring for the + //@@ model. + //@@ + repeated Agent agents = 1; +} + +//@@ +//@@.. cpp:var:: message ModelResponseCache +//@@ +//@@ The response cache setting for the model. +//@@ +message ModelResponseCache +{ + //@@ + //@@ .. cpp::var:: bool enable + //@@ + //@@ Whether or not to use response cache for the model. If True, the + //@@ responses from the model are cached and when identical request + //@@ is encountered, instead of going through the model execution, + //@@ the response from the cache is utilized. By default, response + //@@ cache is disabled for the models. + //@@ + bool enable = 1; +} + +//@@ +//@@ .. cpp:var:: message ModelMetrics +//@@ +//@@ The metrics setting of this model. +//@@ NOTE: Consider reusing this message body for backend metric custom +//@@ configuration. +//@@ +message ModelMetrics +{ + //@@ + //@@ .. cpp:var:: message MetricControl + //@@ + //@@ Override metrics settings of this model. + //@@ + message MetricControl + { + //@@ + //@@ .. cpp:var:: message MetricIdentifier + //@@ + //@@ Specify metrics to be overridden with metric_option. + //@@ + message MetricIdentifier + { + //@@ .. cpp:var:: string family + //@@ + //@@ The name of the metric family to override with the custom value. + //@@ All core histogram metrics reported by Triton are customizable. + //@@ + // https://github.com/triton-inference-server/server/blob/main/docs/user_guide/metrics.md#histograms + //@@ + string family = 1; + } + + //@@ .. cpp:var:: message HistogramOptions + //@@ + //@@ Histogram metrics options. + //@@ + message HistogramOptions + { + //@@ .. cpp:var:: double buckets (repeated) + //@@ + //@@ Repeated double type in ascending order for histogram bucket + //@@ boundaries. Each bucket value represents a range less than or + //@@ equal to itself. The range greater than the largest bucket value + //@@ is allocated implicitly. + //@@ For example, [ -5.0, -2, 0, 3.5, 5 ]. + //@@ + repeated double buckets = 1; + } + + //@@ .. cpp:var:: MetricIdentifier metric_identifier + //@@ + //@@ The identifier defining metrics to be overridden with the + //@@ metric_options. + //@@ + MetricIdentifier metric_identifier = 1; + + //@@ .. cpp:var:: oneof metric_options + //@@ + //@@ The value to override the metrics defined in metric_identifier. + //@@ + oneof metric_options + { + //@@ .. cpp:var:: HistogramOptions histogram_options + //@@ + //@@ Histogram options. + //@@ + HistogramOptions histogram_options = 2; + } + } + + //@@ + //@@ .. cpp::var:: MetricControl metric_control (repeated) + //@@ + //@@ Optional custom configuration for selected metrics. + //@@ + repeated MetricControl metric_control = 1; +} + +//@@ +//@@.. cpp:var:: message ModelConfig +//@@ +//@@ A model configuration. +//@@ +message ModelConfig +{ + //@@ .. cpp:var:: string name + //@@ + //@@ The name of the model. + //@@ + string name = 1; + + //@@ .. cpp:var:: string platform + //@@ + //@@ Additional backend-specific configuration for the model. + //@@ Please refer to the backend documentation on whether this field + //@@ should be specified. + //@@ + string platform = 2; + + //@@ .. cpp:var:: string backend + //@@ + //@@ The backend used by the model. + //@@ + string backend = 17; + + //@@ .. cpp:var:: string runtime + //@@ + //@@ The name of the backend library file used by the model. + //@@ + string runtime = 25; + + //@@ .. cpp:var:: ModelVersionPolicy version_policy + //@@ + //@@ Policy indicating which version(s) of the model will be served. + //@@ + ModelVersionPolicy version_policy = 3; + + //@@ .. cpp:var:: int32 max_batch_size + //@@ + //@@ Maximum batch size allowed for inference. This can only decrease + //@@ what is allowed by the model itself. A max_batch_size value of 0 + //@@ indicates that batching is not allowed for the model and the + //@@ dimension/shape of the input and output tensors must exactly + //@@ match what is specified in the input and output configuration. A + //@@ max_batch_size value > 0 indicates that batching is allowed and + //@@ so the model expects the input tensors to have an additional + //@@ initial dimension for the batching that is not specified in the + //@@ input (for example, if the model supports batched inputs of + //@@ 2-dimensional tensors then the model configuration will specify + //@@ the input shape as [ X, Y ] but the model will expect the actual + //@@ input tensors to have shape [ N, X, Y ]). For max_batch_size > 0 + //@@ returned outputs will also have an additional initial dimension + //@@ for the batch. + //@@ + int32 max_batch_size = 4; + + //@@ .. cpp:var:: ModelInput input (repeated) + //@@ + //@@ The inputs request by the model. + //@@ + repeated ModelInput input = 5; + + //@@ .. cpp:var:: ModelOutput output (repeated) + //@@ + //@@ The outputs produced by the model. + //@@ + repeated ModelOutput output = 6; + + //@@ .. cpp:var:: BatchInput batch_input (repeated) + //@@ + //@@ The model input(s) that the server should use to communicate + //@@ batch related values to the model. + //@@ + repeated BatchInput batch_input = 20; + + //@@ .. cpp:var:: BatchOutput batch_output (repeated) + //@@ + //@@ The outputs produced by the model that requires special handling + //@@ by the model backend. + //@@ + repeated BatchOutput batch_output = 21; + + //@@ .. cpp:var:: ModelOptimizationPolicy optimization + //@@ + //@@ Optimization configuration for the model. If not specified + //@@ then default optimization policy is used. + //@@ + ModelOptimizationPolicy optimization = 12; + + //@@ .. cpp:var:: oneof scheduling_choice + //@@ + //@@ The scheduling policy for the model. If not specified the + //@@ default scheduling policy is used for the model. The default + //@@ policy is to execute each inference request independently. + //@@ + oneof scheduling_choice + { + //@@ .. cpp:var:: ModelDynamicBatching dynamic_batching + //@@ + //@@ If specified, enables the dynamic-batching scheduling + //@@ policy. With dynamic-batching the scheduler may group + //@@ together independent requests into a single batch to + //@@ improve inference throughput. + //@@ + ModelDynamicBatching dynamic_batching = 11; + + //@@ .. cpp:var:: ModelSequenceBatching sequence_batching + //@@ + //@@ If specified, enables the sequence-batching scheduling + //@@ policy. With sequence-batching, inference requests + //@@ with the same correlation ID are routed to the same + //@@ model instance. Multiple sequences of inference requests + //@@ may be batched together into a single batch to + //@@ improve inference throughput. + //@@ + ModelSequenceBatching sequence_batching = 13; + + //@@ .. cpp:var:: ModelEnsembling ensemble_scheduling + //@@ + //@@ If specified, enables the model-ensembling scheduling + //@@ policy. With model-ensembling, inference requests + //@@ will be processed according to the specification, such as an + //@@ execution sequence of models. The input specified in this model + //@@ config will be the input for the ensemble, and the output + //@@ specified will be the output of the ensemble. + //@@ + ModelEnsembling ensemble_scheduling = 15; + } + + //@@ .. cpp:var:: ModelInstanceGroup instance_group (repeated) + //@@ + //@@ Instances of this model. If not specified, one instance + //@@ of the model will be instantiated on each available GPU. + //@@ + repeated ModelInstanceGroup instance_group = 7; + + //@@ .. cpp:var:: string default_model_filename + //@@ + //@@ Optional filename of the model file to use if a + //@@ compute-capability specific model is not specified in + //@@ :cpp:var:`cc_model_filenames`. If not specified the default name + //@@ is 'model.graphdef', 'model.savedmodel', 'model.plan' or + //@@ 'model.pt' depending on the model type. + //@@ + string default_model_filename = 8; + + //@@ .. cpp:var:: map cc_model_filenames + //@@ + //@@ Optional map from CUDA compute capability to the filename of + //@@ the model that supports that compute capability. The filename + //@@ refers to a file within the model version directory. + //@@ + map cc_model_filenames = 9; + + //@@ .. cpp:var:: map metric_tags + //@@ + //@@ Optional metric tags. User-specific key-value pairs for metrics + //@@ reported for this model. These tags are applied to the metrics + //@@ reported on the HTTP metrics port. + //@@ + map metric_tags = 10; + + //@@ .. cpp:var:: map parameters + //@@ + //@@ Optional model parameters. User-specified parameter values. + //@@ + map parameters = 14; + + //@@ .. cpp:var:: ModelWarmup model_warmup (repeated) + //@@ + //@@ Warmup setting of this model. If specified, all instances + //@@ will be run with the request samples in sequence before + //@@ serving the model. + //@@ This field can only be specified if the model is not an ensemble + //@@ model. + //@@ + repeated ModelWarmup model_warmup = 16; + + //@@ .. cpp:var:: ModelOperations model_operations + //@@ + //@@ Optional metadata of the libraries providing custom operations for + //@@ this model. + //@@ + ModelOperations model_operations = 18; + + //@@ .. cpp:var:: ModelTransactionPolicy model_transaction_policy + //@@ + //@@ Optional specification that describes the nature of transactions + //@@ to be expected from the model. + //@@ + ModelTransactionPolicy model_transaction_policy = 19; + + //@@ .. cpp:var:: ModelRepositoryAgents model_repository_agents + //@@ + //@@ Optional specification of the agent(s) that should be invoked + //@@ with repository actions are performed for this model. + //@@ + ModelRepositoryAgents model_repository_agents = 23; + + //@@ .. cpp:var:: ModelResponseCache response_cache + //@@ + //@@ Optional setting for utilizing the response cache for this + //@@ model. + //@@ + ModelResponseCache response_cache = 24; + + //@@ .. cpp:var:: ModelMetrics model_metrics + //@@ + //@@ Optional setting for custom metrics configuration for this model. + //@@ Application default is applied to metrics that are not specified. + //@@ + ModelMetrics model_metrics = 26; +} diff --git a/lib/llm/src/grpc/service.rs b/lib/llm/src/grpc/service.rs new file mode 100644 index 0000000000..3f1007fddc --- /dev/null +++ b/lib/llm/src/grpc/service.rs @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +pub mod kserve; +pub mod openai; diff --git a/lib/llm/src/grpc/service/kserve.rs b/lib/llm/src/grpc/service/kserve.rs new file mode 100644 index 0000000000..298c35c30e --- /dev/null +++ b/lib/llm/src/grpc/service/kserve.rs @@ -0,0 +1,625 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::pin::Pin; +use std::sync::Arc; + +use crate::grpc::service::kserve::inference::DataType; +use crate::grpc::service::kserve::inference::ModelInput; +use crate::grpc::service::kserve::inference::ModelOutput; +use crate::http::service::Metrics; +use crate::http::service::metrics; + +use crate::discovery::ModelManager; +use crate::request_template::RequestTemplate; +use anyhow::Result; +use derive_builder::Builder; +use dynamo_async_openai::types::{CompletionFinishReason, CreateCompletionRequest, Prompt}; +use dynamo_runtime::transports::etcd; +use futures::pin_mut; +use tokio::task::JoinHandle; +use tokio_stream::{Stream, StreamExt}; +use tokio_util::sync::CancellationToken; + +use crate::grpc::service::openai::{completion_response_stream, get_parsing_options}; +use tonic::{Request, Response, Status, transport::Server}; + +use crate::protocols::openai::completions::{ + NvCreateCompletionRequest, NvCreateCompletionResponse, +}; + +pub mod inference { + tonic::include_proto!("inference"); +} +use inference::grpc_inference_service_server::{GrpcInferenceService, GrpcInferenceServiceServer}; +use inference::{ + InferParameter, ModelConfig, ModelConfigRequest, ModelConfigResponse, ModelInferRequest, + ModelInferResponse, ModelMetadataRequest, ModelMetadataResponse, ModelStreamInferResponse, +}; + +/// [gluo TODO] 'metrics' are for HTTP service and there is HTTP endpoint +/// for it as part of HTTP service. Should we always start HTTP service up +/// for non-inference? +pub struct State { + metrics: Arc, + manager: Arc, + etcd_client: Option, +} + +impl State { + pub fn new(manager: Arc) -> Self { + Self { + manager, + metrics: Arc::new(Metrics::default()), + etcd_client: None, + } + } + + pub fn new_with_etcd(manager: Arc, etcd_client: Option) -> Self { + Self { + manager, + metrics: Arc::new(Metrics::default()), + etcd_client, + } + } + + /// Get the Prometheus [`Metrics`] object which tracks request counts and inflight requests + pub fn metrics_clone(&self) -> Arc { + self.metrics.clone() + } + + pub fn manager(&self) -> &ModelManager { + Arc::as_ref(&self.manager) + } + + pub fn manager_clone(&self) -> Arc { + self.manager.clone() + } + + pub fn etcd_client(&self) -> Option<&etcd::Client> { + self.etcd_client.as_ref() + } +} + +#[derive(Clone)] +pub struct KserveService { + // The state we share with every request handler + state: Arc, + + port: u16, + host: String, + request_template: Option, +} + +#[derive(Clone, Builder)] +#[builder(pattern = "owned", build_fn(private, name = "build_internal"))] +pub struct KserveServiceConfig { + #[builder(default = "8787")] + port: u16, + + #[builder(setter(into), default = "String::from(\"0.0.0.0\")")] + host: String, + + #[builder(default = "None")] + request_template: Option, + + #[builder(default = "None")] + etcd_client: Option, +} + +impl KserveService { + pub fn builder() -> KserveServiceConfigBuilder { + KserveServiceConfigBuilder::default() + } + + pub fn state_clone(&self) -> Arc { + self.state.clone() + } + + pub fn state(&self) -> &State { + Arc::as_ref(&self.state) + } + + pub fn model_manager(&self) -> &ModelManager { + self.state().manager() + } + + pub async fn spawn(&self, cancel_token: CancellationToken) -> JoinHandle> { + let this = self.clone(); + tokio::spawn(async move { this.run(cancel_token).await }) + } + + pub async fn run(&self, cancel_token: CancellationToken) -> Result<()> { + let address = format!("{}:{}", self.host, self.port); + tracing::info!(address, "Starting KServe gRPC service on: {address}"); + + let observer = cancel_token.child_token(); + Server::builder() + .add_service(GrpcInferenceServiceServer::new(self.clone())) + .serve_with_shutdown(address.parse()?, observer.cancelled_owned()) + .await + .inspect_err(|_| cancel_token.cancel())?; + + Ok(()) + } +} + +impl KserveServiceConfigBuilder { + pub fn build(self) -> Result { + let config: KserveServiceConfig = self.build_internal()?; + + let model_manager = Arc::new(ModelManager::new()); + let state = Arc::new(State::new_with_etcd(model_manager, config.etcd_client)); + + // enable prometheus metrics + let registry = metrics::Registry::new(); + state.metrics_clone().register(®istry)?; + + Ok(KserveService { + state, + port: config.port, + host: config.host, + request_template: config.request_template, + }) + } + + pub fn with_request_template(mut self, request_template: Option) -> Self { + self.request_template = Some(request_template); + self + } + + pub fn with_etcd_client(mut self, etcd_client: Option) -> Self { + self.etcd_client = Some(etcd_client); + self + } +} + +#[tonic::async_trait] +impl GrpcInferenceService for KserveService { + async fn model_infer( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let request_id = request.id.clone(); + let mut completion_request: NvCreateCompletionRequest = request + .try_into() + .map_err(|e| Status::invalid_argument(format!("Failed to parse request: {}", e)))?; + + if completion_request.inner.stream.unwrap_or(false) { + // return error that streaming is not supported + return Err(Status::invalid_argument( + "Streaming is not supported for this endpoint", + )); + } + + // Apply template values if present + if let Some(template) = self.request_template.as_ref() { + if completion_request.inner.model.is_empty() { + completion_request.inner.model = template.model.clone(); + } + if completion_request.inner.temperature.unwrap_or(0.0) == 0.0 { + completion_request.inner.temperature = Some(template.temperature); + } + if completion_request.inner.max_tokens.unwrap_or(0) == 0 { + completion_request.inner.max_tokens = Some(template.max_completion_tokens); + } + } + + let model = completion_request.inner.model.clone(); + let parsing_options = get_parsing_options(self.state.manager(), &model); + + let stream = completion_response_stream(self.state_clone(), completion_request).await?; + + let completion_response = + NvCreateCompletionResponse::from_annotated_stream(stream, parsing_options) + .await + .map_err(|e| { + tracing::error!("Failed to fold completions stream: {:?}", e); + Status::internal("Failed to fold completions stream") + })?; + + let mut reply: ModelInferResponse = completion_response + .try_into() + .map_err(|e| Status::invalid_argument(format!("Failed to parse response: {}", e)))?; + + reply.id = request_id; + + Ok(Response::new(reply)) + } + + type ModelStreamInferStream = + Pin> + Send + 'static>>; + + async fn model_stream_infer( + &self, + request: Request>, + ) -> Result, Status> { + let mut request_stream = request.into_inner(); + let state = self.state_clone(); + let template = self.request_template.clone(); + let output = async_stream::try_stream! { + // [gluo FIXME] should be able to demux request / response streaming + // await requests in a separate task until cancellation / completion, + // and passing AsyncEngineStream for each request to the response stream + // which will be collectively polling. + while let Some(request) = request_stream.next().await { + // Must keep track of 'request_id' which will be returned in corresponding response + let request_id: String; + let mut completion_request: NvCreateCompletionRequest = match request { + Err(e) => { + tracing::error!("Unexpected gRPC failed to read request: {}", e); + yield ModelStreamInferResponse { + error_message: e.to_string(), + infer_response: None + }; + continue; + } + Ok(request) => { + request_id = request.id.clone(); + request.try_into().map_err(|e| { + Status::invalid_argument(format!("Failed to parse request: {}", e)) + })? + } + }; + + // Apply template values if present + if let Some(template) = &template { + if completion_request.inner.model.is_empty() { + completion_request.inner.model = template.model.clone(); + } + if completion_request.inner.temperature.unwrap_or(0.0) == 0.0 { + completion_request.inner.temperature = Some(template.temperature); + } + if completion_request.inner.max_tokens.unwrap_or(0) == 0 { + completion_request.inner.max_tokens = Some(template.max_completion_tokens); + } + } + + let model = completion_request.inner.model.clone(); + let parsing_options = get_parsing_options(state.manager(), &model); + + let streaming = completion_request.inner.stream.unwrap_or(false); + + let stream = completion_response_stream(state.clone(), completion_request).await?; + + if streaming { + pin_mut!(stream); + while let Some(response) = stream.next().await { + match response.data { + Some(data) => { + let mut reply = ModelStreamInferResponse::try_from(data).map_err(|e| { + Status::invalid_argument(format!("Failed to parse response: {}", e)) + })?; + if reply.infer_response.is_some() { + reply.infer_response.as_mut().unwrap().id = request_id.clone(); + } + yield reply; + }, + None => { + // Skip if no data is present, the response is for annotation + }, + } + } + } else { + let completion_response = NvCreateCompletionResponse::from_annotated_stream(stream, parsing_options) + .await + .map_err(|e| { + tracing::error!( + "Failed to fold completions stream: {:?}", + e + ); + Status::internal("Failed to fold completions stream") + })?; + + let mut response: ModelStreamInferResponse = completion_response.try_into().map_err(|e| { + Status::invalid_argument(format!("Failed to parse response: {}", e)) + })?; + if response.infer_response.is_some() { + response.infer_response.as_mut().unwrap().id = request_id.clone(); + } + yield response; + } + } + }; + + Ok(Response::new( + Box::pin(output) as Self::ModelStreamInferStream + )) + } + + async fn model_metadata( + &self, + request: Request, + ) -> Result, Status> { + let models = self.state.manager().list_completions_models(); + let request_model_name = &request.into_inner().name; + if let Some(model_name) = models.into_iter().find(|n| request_model_name == n) { + return Ok(Response::new(ModelMetadataResponse { + name: model_name, + versions: vec!["1".to_string()], + platform: "dynamo".to_string(), + inputs: vec![ + inference::model_metadata_response::TensorMetadata { + name: "text_input".to_string(), + datatype: "BYTES".to_string(), + shape: vec![1], + }, + inference::model_metadata_response::TensorMetadata { + name: "streaming".to_string(), + datatype: "BOOL".to_string(), + shape: vec![1], + }, + ], + outputs: vec![ + inference::model_metadata_response::TensorMetadata { + name: "text_output".to_string(), + datatype: "BYTES".to_string(), + shape: vec![-1], + }, + inference::model_metadata_response::TensorMetadata { + name: "finish_reason".to_string(), + datatype: "BYTES".to_string(), + shape: vec![-1], + }, + ], + })); + } + Err(Status::not_found(format!( + "Model '{}' not found", + request_model_name + ))) + } + + async fn model_config( + &self, + request: Request, + ) -> Result, Status> { + let models = self.state.manager().list_completions_models(); + let request_model_name = &request.into_inner().name; + if let Some(model_name) = models.into_iter().find(|n| request_model_name == n) { + let config = ModelConfig { + name: model_name, + platform: "dynamo".to_string(), + backend: "dynamo".to_string(), + input: vec![ + ModelInput { + name: "text_input".to_string(), + data_type: DataType::TypeString as i32, + dims: vec![1], + ..Default::default() + }, + ModelInput { + name: "streaming".to_string(), + data_type: DataType::TypeBool as i32, + dims: vec![1], + optional: true, + ..Default::default() + }, + ], + output: vec![ + ModelOutput { + name: "text_output".to_string(), + data_type: DataType::TypeString as i32, + dims: vec![-1], + ..Default::default() + }, + ModelOutput { + name: "finish_reason".to_string(), + data_type: DataType::TypeString as i32, + dims: vec![-1], + ..Default::default() + }, + ], + ..Default::default() + }; + return Ok(Response::new(ModelConfigResponse { + config: Some(config), + })); + } + Err(Status::not_found(format!( + "Model '{}' not found", + request_model_name + ))) + } +} + +impl TryFrom for NvCreateCompletionRequest { + type Error = Status; + + fn try_from(request: ModelInferRequest) -> Result { + // Protocol requires if `raw_input_contents` is used to hold input data, + // it must be used for all inputs. + if !request.raw_input_contents.is_empty() + && request.inputs.len() != request.raw_input_contents.len() + { + return Err(Status::invalid_argument( + "`raw_input_contents` must be used for all inputs", + )); + } + + // iterate through inputs + let mut text_input = None; + let mut stream = false; + for (idx, input) in request.inputs.iter().enumerate() { + match input.name.as_str() { + "text_input" => { + if input.datatype != "BYTES" { + return Err(Status::invalid_argument(format!( + "Expected 'text_input' to be of type BYTES for string input, got {:?}", + input.datatype + ))); + } + if input.shape != vec![1] && input.shape != vec![1, 1] { + return Err(Status::invalid_argument(format!( + "Expected 'text_input' to have shape [1], got {:?}", + input.shape + ))); + } + match &input.contents { + Some(content) => { + let bytes = &content.bytes_contents[0]; + text_input = Some(String::from_utf8_lossy(bytes).to_string()); + } + None => { + let raw_input = + request.raw_input_contents.get(idx).ok_or_else(|| { + Status::invalid_argument("Missing raw input for 'text_input'") + })?; + if raw_input.len() < 4 { + return Err(Status::invalid_argument( + "'text_input' raw input must be length-prefixed (>= 4 bytes)", + )); + } + // We restrict the 'text_input' only contain one element, only need to + // parse the first element. Skip first four bytes that is used to store + // the length of the input. + text_input = Some(String::from_utf8_lossy(&raw_input[4..]).to_string()); + } + } + } + "streaming" | "stream" => { + if input.datatype != "BOOL" { + return Err(Status::invalid_argument(format!( + "Expected '{}' to be of type BOOL, got {:?}", + input.name, input.datatype + ))); + } + if input.shape != vec![1] { + return Err(Status::invalid_argument(format!( + "Expected 'stream' to have shape [1], got {:?}", + input.shape + ))); + } + match &input.contents { + Some(content) => { + stream = content.bool_contents[0]; + } + None => { + let raw_input = + request.raw_input_contents.get(idx).ok_or_else(|| { + Status::invalid_argument("Missing raw input for 'stream'") + })?; + if raw_input.is_empty() { + return Err(Status::invalid_argument( + "'stream' raw input must contain at least one byte", + )); + } + stream = raw_input[0] != 0; + } + } + } + _ => { + return Err(Status::invalid_argument(format!( + "Invalid input name: {}, supported inputs are 'text_input', 'stream'", + input.name + ))); + } + } + } + + // return error if text_input is None + let text_input = match text_input { + Some(input) => input, + None => { + return Err(Status::invalid_argument( + "Missing required input: 'text_input'", + )); + } + }; + + Ok(NvCreateCompletionRequest { + inner: CreateCompletionRequest { + model: request.model_name, + prompt: Prompt::String(text_input), + stream: Some(stream), + user: if request.id.is_empty() { + None + } else { + Some(request.id.clone()) + }, + ..Default::default() + }, + common: Default::default(), + nvext: None, + }) + } +} + +impl TryFrom for ModelInferResponse { + type Error = anyhow::Error; + + fn try_from(response: NvCreateCompletionResponse) -> Result { + let mut outputs = vec![]; + let mut text_output = vec![]; + let mut finish_reason = vec![]; + for choice in &response.inner.choices { + text_output.push(choice.text.clone()); + if let Some(reason) = choice.finish_reason.as_ref() { + match reason { + CompletionFinishReason::Stop => { + finish_reason.push("stop".to_string()); + } + CompletionFinishReason::Length => { + finish_reason.push("length".to_string()); + } + CompletionFinishReason::ContentFilter => { + finish_reason.push("content_filter".to_string()); + } + } + } + } + outputs.push(inference::model_infer_response::InferOutputTensor { + name: "text_output".to_string(), + datatype: "BYTES".to_string(), + shape: vec![text_output.len() as i64], + contents: Some(inference::InferTensorContents { + bytes_contents: text_output + .into_iter() + .map(|text| text.as_bytes().to_vec()) + .collect(), + ..Default::default() + }), + ..Default::default() + }); + outputs.push(inference::model_infer_response::InferOutputTensor { + name: "finish_reason".to_string(), + datatype: "BYTES".to_string(), + shape: vec![finish_reason.len() as i64], + contents: Some(inference::InferTensorContents { + bytes_contents: finish_reason + .into_iter() + .map(|text| text.as_bytes().to_vec()) + .collect(), + ..Default::default() + }), + ..Default::default() + }); + + Ok(ModelInferResponse { + model_name: response.inner.model, + model_version: "1".to_string(), + id: response.inner.id, + outputs, + parameters: ::std::collections::HashMap::::new(), + raw_output_contents: vec![], + }) + } +} + +impl TryFrom for ModelStreamInferResponse { + type Error = anyhow::Error; + + fn try_from(response: NvCreateCompletionResponse) -> Result { + match ModelInferResponse::try_from(response) { + Ok(response) => Ok(ModelStreamInferResponse { + infer_response: Some(response), + ..Default::default() + }), + Err(e) => Ok(ModelStreamInferResponse { + infer_response: None, + error_message: format!("Failed to convert response: {}", e), + }), + } + } +} diff --git a/lib/llm/src/grpc/service/openai.rs b/lib/llm/src/grpc/service/openai.rs new file mode 100644 index 0000000000..943731e07a --- /dev/null +++ b/lib/llm/src/grpc/service/openai.rs @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use dynamo_runtime::{ + engine::AsyncEngineContext, + pipeline::{AsyncEngineContextProvider, Context}, + protocols::annotated::AnnotationsProvider, +}; +use futures::{Stream, StreamExt, stream}; +use std::sync::Arc; + +use crate::discovery::ModelManager; +use crate::protocols::openai::{ + ParsingOptions, + completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, +}; +use crate::types::Annotated; + +use super::kserve; + +// [gluo NOTE] These are common utilities that should be shared between frontends +use crate::http::service::{ + disconnect::{ConnectionHandle, create_connection_monitor}, + metrics::{Endpoint, ResponseMetricCollector}, +}; +use crate::{http::service::metrics::InflightGuard, preprocessor::LLMMetricAnnotation}; + +use tonic::Status; + +/// Dynamo Annotation for the request ID +pub const ANNOTATION_REQUEST_ID: &str = "request_id"; + +// [gluo NOTE] strip down version of lib/llm/src/http/service/openai.rs +// dupliating it here as the original file has coupling with HTTP objects. + +/// OpenAI Completions Request Handler +/// +/// This method will handle the incoming request for the `/v1/completions endpoint`. The endpoint is a "source" +/// for an [`super::OpenAICompletionsStreamingEngine`] and will return a stream of +/// responses which will be forward to the client. +/// +/// Note: For all requests, streaming or non-streaming, we always call the engine with streaming enabled. For +/// non-streaming requests, we will fold the stream into a single response as part of this handler. +pub async fn completion_response_stream( + state: Arc, + request: NvCreateCompletionRequest, +) -> Result>, Status> { + // create the context for the request + // [WIP] from request id. + let request_id = get_or_create_request_id(request.inner.user.as_deref()); + let request = Context::with_id(request, request_id.clone()); + let context = request.context(); + + // create the connection handles + let (mut connection_handle, stream_handle) = create_connection_monitor(context.clone()).await; + + let streaming = request.inner.stream.unwrap_or(false); + // update the request to always stream + let request = request.map(|mut req| { + req.inner.stream = Some(true); + req + }); + + // todo - make the protocols be optional for model name + // todo - when optional, if none, apply a default + let model = &request.inner.model; + + // todo - error handling should be more robust + let engine = state + .manager() + .get_completions_engine(model) + .map_err(|_| Status::not_found("model not found"))?; + + let inflight_guard = + state + .metrics_clone() + .create_inflight_guard(model, Endpoint::Completions, streaming); + + let mut response_collector = state.metrics_clone().create_response_collector(model); + + // prepare to process any annotations + let annotations = request.annotations(); + + // issue the generate call on the engine + let stream = engine + .generate(request) + .await + .map_err(|e| Status::internal(format!("Failed to generate completions: {}", e)))?; + + // capture the context to cancel the stream if the client disconnects + let ctx = stream.context(); + + // prepare any requested annotations + let annotations = annotations.map_or(Vec::new(), |annotations| { + annotations + .iter() + .filter_map(|annotation| { + if annotation == ANNOTATION_REQUEST_ID { + Annotated::::from_annotation( + ANNOTATION_REQUEST_ID, + &request_id, + ) + .ok() + } else { + None + } + }) + .collect::>() + }); + + // apply any annotations to the front of the stream + let stream = stream::iter(annotations).chain(stream); + + // Tap on the stream to collect response metrics + let stream = stream.inspect(move |response| { + process_metrics_only(response, &mut response_collector); + }); + + let stream = grpc_monitor_for_disconnects(stream, ctx, inflight_guard, stream_handle); + + // if we got here, then we will return a response and the potentially long running task has completed successfully + // without need to be cancelled. + connection_handle.disarm(); + + Ok(stream) +} + +/// This method will consume an AsyncEngineStream and monitor for disconnects or context cancellation. +/// This is gRPC variant of `monitor_for_disconnects` as that implementation has SSE specific handling. +/// Should decouple and reuse `monitor_for_disconnects` +/// +/// Uses `tokio::select!` to choose between receiving responses from the source stream or detecting when +/// the context is stopped. If the context is stopped, we break the stream. If the source stream ends +/// naturally, we mark the request as successful and send the final `[DONE]` event. +pub fn grpc_monitor_for_disconnects( + stream: impl Stream>, + context: Arc, + mut inflight_guard: InflightGuard, + mut stream_handle: ConnectionHandle, +) -> impl Stream> { + stream_handle.arm(); + async_stream::stream! { + tokio::pin!(stream); + loop { + tokio::select! { + event = stream.next() => { + match event { + Some(response) => { + yield response; + } + None => { + // Stream ended normally + inflight_guard.mark_ok(); + stream_handle.disarm(); + break; + } + } + } + _ = context.stopped() => { + tracing::trace!("Context stopped; breaking stream"); + break; + } + } + } + } +} + +fn process_metrics_only( + annotated: &Annotated, + response_collector: &mut ResponseMetricCollector, +) { + // update metrics + if let Ok(Some(metrics)) = LLMMetricAnnotation::from_annotation(annotated) { + response_collector.observe_current_osl(metrics.output_tokens); + response_collector.observe_response(metrics.input_tokens, metrics.chunk_tokens); + } +} + +/// Get the request ID from a primary source, or lastly create a new one if not present +fn get_or_create_request_id(primary: Option<&str>) -> String { + // Try to get the request ID from the primary source + if let Some(primary) = primary + && let Ok(uuid) = uuid::Uuid::parse_str(primary) + { + return uuid.to_string(); + } + + // Try to parse the request ID as a UUID, or generate a new one if missing/invalid + let uuid = uuid::Uuid::new_v4(); + uuid.to_string() +} + +pub fn get_parsing_options(manager: &ModelManager, model: &str) -> ParsingOptions { + let tool_call_parser = manager.get_model_tool_call_parser(model); + let reasoning_parser = None; // TODO: Implement reasoning parser + + ParsingOptions::new(tool_call_parser, reasoning_parser) +} diff --git a/lib/llm/src/http/service/openai.rs b/lib/llm/src/http/service/openai.rs index b8fbc33ec6..30b7df6da7 100644 --- a/lib/llm/src/http/service/openai.rs +++ b/lib/llm/src/http/service/openai.rs @@ -31,7 +31,6 @@ use super::{ metrics::{Endpoint, ResponseMetricCollector}, service_v2, }; -use crate::preprocessor::LLMMetricAnnotation; use crate::protocols::openai::chat_completions::aggregator::ChatCompletionAggregator; use crate::protocols::openai::{ ParsingOptions, @@ -42,6 +41,7 @@ use crate::protocols::openai::{ }; use crate::request_template::RequestTemplate; use crate::types::Annotated; +use crate::{discovery::ModelManager, preprocessor::LLMMetricAnnotation}; use dynamo_runtime::logging::get_distributed_tracing_context; use tracing::Instrument; @@ -195,8 +195,8 @@ fn get_or_create_request_id(primary: Option<&str>, headers: &HeaderMap) -> Strin uuid.to_string() } -fn get_parsing_options(state: &Arc, model: &str) -> ParsingOptions { - let tool_call_parser = state.manager().get_model_tool_call_parser(model); +fn get_parsing_options(manager: &ModelManager, model: &str) -> ParsingOptions { + let tool_call_parser = manager.get_model_tool_call_parser(model); let reasoning_parser = None; // TODO: Implement reasoning parser ParsingOptions::new(tool_call_parser, reasoning_parser) @@ -274,7 +274,7 @@ async fn completions( .get_completions_engine(model) .map_err(|_| ErrorMessage::model_not_found())?; - let parsing_options = get_parsing_options(&state, model); + let parsing_options = get_parsing_options(state.manager(), model); let mut inflight_guard = state @@ -501,7 +501,7 @@ async fn chat_completions( .get_chat_completions_engine(model) .map_err(|_| ErrorMessage::model_not_found())?; - let parsing_options = get_parsing_options(&state, model); + let parsing_options = get_parsing_options(state.manager(), model); let mut inflight_guard = state @@ -736,7 +736,7 @@ async fn responses( .get_chat_completions_engine(model) .map_err(|_| ErrorMessage::model_not_found())?; - let parsing_options = get_parsing_options(&state, model); + let parsing_options = get_parsing_options(state.manager(), model); let mut inflight_guard = state diff --git a/lib/llm/src/lib.rs b/lib/llm/src/lib.rs index fbca78a7fd..786eddf78f 100644 --- a/lib/llm/src/lib.rs +++ b/lib/llm/src/lib.rs @@ -18,6 +18,7 @@ pub mod endpoint_type; pub mod engines; pub mod entrypoint; pub mod gguf; +pub mod grpc; pub mod http; pub mod hub; // pub mod key_value_store; diff --git a/lib/llm/tests/kserve_service.rs b/lib/llm/tests/kserve_service.rs new file mode 100644 index 0000000000..abb23a35aa --- /dev/null +++ b/lib/llm/tests/kserve_service.rs @@ -0,0 +1,1055 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +pub mod kserve_test { + // For using gRPC client for test + pub mod inference { + tonic::include_proto!("inference"); + } + use inference::grpc_inference_service_client::GrpcInferenceServiceClient; + use inference::{ + DataType, ModelConfigRequest, ModelInferRequest, ModelInferResponse, ModelMetadataRequest, + }; + + use anyhow::Error; + use async_stream::stream; + use dynamo_llm::grpc::service::kserve::KserveService; + use dynamo_llm::protocols::{ + Annotated, + openai::{ + chat_completions::{ + NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse, + }, + completions::{NvCreateCompletionRequest, NvCreateCompletionResponse}, + }, + }; + use dynamo_runtime::{ + CancellationToken, + pipeline::{ + AsyncEngine, AsyncEngineContextProvider, ManyOut, ResponseStream, SingleIn, async_trait, + }, + }; + use rstest::*; + use std::sync::Arc; + use std::time::Duration; + use tokio::time::timeout; + use tonic::{Request, Response, transport::Channel}; + + use dynamo_async_openai::types::Prompt; + + struct SplitEngine {} + + // Add a new long-running test engine + struct LongRunningEngine { + delay_ms: u64, + cancelled: Arc, + } + + impl LongRunningEngine { + fn new(delay_ms: u64) -> Self { + Self { + delay_ms, + cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)), + } + } + + fn was_cancelled(&self) -> bool { + self.cancelled.load(std::sync::atomic::Ordering::Acquire) + } + + // Wait for the duration of generation delay to ensure the generate stream + // has been terminated early (`was_cancelled` remains true). + async fn wait_for_delay(&self) { + tokio::time::sleep(std::time::Duration::from_millis(self.delay_ms)).await; + } + } + + #[async_trait] + impl + AsyncEngine< + SingleIn, + ManyOut>, + Error, + > for SplitEngine + { + async fn generate( + &self, + request: SingleIn, + ) -> Result>, Error> { + let (request, context) = request.transfer(()); + let ctx = context.context(); + + // let generator = NvCreateChatCompletionStreamResponse::generator(request.model.clone()); + let generator = request.response_generator(ctx.id().to_string()); + + let word_list: Vec = match request.inner.prompt { + Prompt::String(str) => str.split(' ').map(|s| s.to_string()).collect(), + _ => { + return Err(Error::msg("SplitEngine only support prompt type String"))?; + } + }; + let stream = stream! { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + for word in word_list { + yield Annotated::from_data(generator.create_choice(0, Some(word.to_string()), None, None)); + } + }; + + Ok(ResponseStream::new(Box::pin(stream), ctx)) + } + } + + #[async_trait] + impl + AsyncEngine< + SingleIn, + ManyOut>, + Error, + > for LongRunningEngine + { + async fn generate( + &self, + request: SingleIn, + ) -> Result>, Error> { + let (_request, context) = request.transfer(()); + let ctx = context.context(); + + tracing::info!( + "LongRunningEngine: Starting generation with {}ms delay", + self.delay_ms + ); + + let cancelled_flag = self.cancelled.clone(); + let delay_ms = self.delay_ms; + + let ctx_clone = ctx.clone(); + let stream = async_stream::stream! { + + // the stream can be dropped or it can be cancelled + // either way we consider this a cancellation + cancelled_flag.store(true, std::sync::atomic::Ordering::SeqCst); + + tokio::select! { + _ = tokio::time::sleep(std::time::Duration::from_millis(delay_ms)) => { + // the stream went to completion + cancelled_flag.store(false, std::sync::atomic::Ordering::SeqCst); + + } + _ = ctx_clone.stopped() => { + cancelled_flag.store(true, std::sync::atomic::Ordering::SeqCst); + } + } + + yield Annotated::::from_annotation("event.dynamo.test.sentinel", &"DONE".to_string()).expect("Failed to create annotated response"); + }; + + Ok(ResponseStream::new(Box::pin(stream), ctx)) + } + } + + struct AlwaysFailEngine {} + + #[async_trait] + impl + AsyncEngine< + SingleIn, + ManyOut>, + Error, + > for AlwaysFailEngine + { + async fn generate( + &self, + _request: SingleIn, + ) -> Result>, Error> { + Err(Error::msg("Always fail"))? + } + } + + #[async_trait] + impl + AsyncEngine< + SingleIn, + ManyOut>, + Error, + > for AlwaysFailEngine + { + async fn generate( + &self, + _request: SingleIn, + ) -> Result>, Error> { + Err(Error::msg("Always fail"))? + } + } + + /// Wait for the HTTP service to be ready by checking its health endpoint + async fn get_ready_client(port: u16, timeout_secs: u64) -> GrpcInferenceServiceClient { + let start = tokio::time::Instant::now(); + let timeout = tokio::time::Duration::from_secs(timeout_secs); + loop { + let address = format!("http://0.0.0.0:{}", port); + match GrpcInferenceServiceClient::connect(address).await { + Ok(client) => return client, + Err(_) if start.elapsed() < timeout => { + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + } + Err(e) => panic!("Service failed to start within timeout: {}", e), + } + } + } + + #[fixture] + fn text_input( + #[default("dummy input")] text: &str, + ) -> inference::model_infer_request::InferInputTensor { + inference::model_infer_request::InferInputTensor { + name: "text_input".into(), + datatype: "BYTES".into(), + shape: vec![1], + contents: Some(inference::InferTensorContents { + bytes_contents: vec![text.into()], + ..Default::default() + }), + ..Default::default() + } + } + + #[fixture] + fn service_with_engines( + #[default(8990)] port: u16, + ) -> ( + KserveService, + Arc, + Arc, + Arc, + ) { + let service = KserveService::builder().port(port).build().unwrap(); + let manager = service.model_manager(); + + let split = Arc::new(SplitEngine {}); + let failure = Arc::new(AlwaysFailEngine {}); + let long_running = Arc::new(LongRunningEngine::new(1_000)); + + manager + .add_completions_model("split", split.clone()) + .unwrap(); + manager + .add_chat_completions_model("failure", failure.clone()) + .unwrap(); + manager + .add_completions_model("failure", failure.clone()) + .unwrap(); + manager + .add_completions_model("long_running", long_running.clone()) + .unwrap(); + + (service, split, failure, long_running) + } + + struct RunningService { + token: CancellationToken, + } + + impl RunningService { + fn spawn(service: KserveService) -> Self { + let token = CancellationToken::new(); + tokio::spawn({ + let t = token.clone(); + async move { service.run(t).await } + }); + Self { token } + } + } + + impl Drop for RunningService { + fn drop(&mut self) { + self.token.cancel(); + } + } + + // Tests may run in parallel, use this enum to keep track of port used for different + // test cases + enum TestPort { + InferFailure = 8988, + InferSuccess = 8989, + StreamInferFailure = 8990, + StreamInferSuccess = 8991, + InferCancellation = 8992, + StreamInferCancellation = 8993, + ModelInfo = 8994, + } + + #[rstest] + #[tokio::test] + async fn test_infer_failure( + #[with(TestPort::InferFailure as u16)] service_with_engines: ( + KserveService, + Arc, + Arc, + Arc, + ), + text_input: inference::model_infer_request::InferInputTensor, + ) { + // start server + let _running = RunningService::spawn(service_with_engines.0); + + // create client and send request to unregistered model + let mut client = get_ready_client(TestPort::InferFailure as u16, 5).await; + + // unknown_model + let request = tonic::Request::new(ModelInferRequest { + model_name: "Tonic".into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![text_input.clone()], + ..Default::default() + }); + + let response = client.model_infer(request).await; + assert!(response.is_err()); + let err = response.unwrap_err(); + assert_eq!( + err.code(), + tonic::Code::NotFound, + "Expected NotFound error for unregistered model, get {}", + err + ); + + // missing input + let request = tonic::Request::new(ModelInferRequest { + model_name: "split".into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![], + ..Default::default() + }); + + let response = client.model_infer(request).await; + assert!(response.is_err()); + let err = response.unwrap_err(); + assert_eq!( + err.code(), + tonic::Code::InvalidArgument, + "Expected InvalidArgument error for missing input, get {}", + err + ); + + // request streaming + let request = tonic::Request::new(ModelInferRequest { + model_name: "split".into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![ + text_input.clone(), + inference::model_infer_request::InferInputTensor { + name: "streaming".into(), + datatype: "BOOL".into(), + shape: vec![1], + contents: Some(inference::InferTensorContents { + bool_contents: vec![true], + ..Default::default() + }), + ..Default::default() + }, + ], + ..Default::default() + }); + + let response = client.model_infer(request).await; + assert!(response.is_err()); + let err = response.unwrap_err(); + assert_eq!( + err.code(), + tonic::Code::InvalidArgument, + "Expected InvalidArgument error for streaming, get {}", + err + ); + // assert "stream" in error message + assert!( + err.message().contains("Streaming is not supported"), + "Expected error message to contain 'Streaming is not supported', got: {}", + err.message() + ); + + // AlwaysFailEngine + let request = tonic::Request::new(ModelInferRequest { + model_name: "failure".into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![text_input.clone()], + ..Default::default() + }); + + let response = client.model_infer(request).await; + assert!(response.is_err()); + let err = response.unwrap_err(); + assert_eq!( + err.code(), + tonic::Code::Internal, + "Expected Internal error for streaming, get {}", + err + ); + assert!( + err.message().contains("Failed to generate completions:"), + "Expected error message to contain 'Failed to generate completions:', got: {}", + err.message() + ); + } + + #[rstest] + #[tokio::test] + async fn test_infer_success( + #[with(TestPort::InferSuccess as u16)] service_with_engines: ( + KserveService, + Arc, + Arc, + Arc, + ), + text_input: inference::model_infer_request::InferInputTensor, + ) { + // start server + let _running = RunningService::spawn(service_with_engines.0); + + let mut client = get_ready_client(TestPort::InferSuccess as u16, 5).await; + + let model_name = "split"; + let request = tonic::Request::new(ModelInferRequest { + model_name: model_name.into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![text_input.clone()], + ..Default::default() + }); + + let response = client.model_infer(request).await.unwrap(); + validate_infer_response(response, model_name); + + // Input data in raw_input_content + let mut text_input = text_input.clone(); + text_input.contents = None; // Clear contents to use raw_input_contents + let text_input_str = "dummy input"; + let input_len = text_input_str.len() as u32; + let mut serialized_input = input_len.to_le_bytes().to_vec(); + serialized_input.extend_from_slice(text_input_str.as_bytes()); + let request = tonic::Request::new(ModelInferRequest { + model_name: model_name.into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![text_input], + raw_input_contents: vec![serialized_input], + ..Default::default() + }); + let response = client.model_infer(request).await.unwrap(); + validate_infer_response(response, model_name); + } + + fn validate_infer_response(response: Response, model_name: &str) { + assert_eq!( + response.get_ref().model_name, + model_name, + "Expected response of the same model name", + ); + for output in &response.get_ref().outputs { + match output.name.as_str() { + "text_output" => { + assert_eq!( + output.datatype, "BYTES", + "Expected 'text_output' to have datatype 'BYTES'" + ); + assert_eq!( + output.shape, + vec![1], + "Expected 'text_output' to have shape [1]" + ); + let expected_output: Vec> = vec!["dummyinput".into()]; + assert_eq!( + output.contents.as_ref().unwrap().bytes_contents, + expected_output, + "Expected 'text_output' to contain 'dummy input'" + ); + } + "finish_reason" => { + assert_eq!( + output.datatype, "BYTES", + "Expected 'finish_reason' to have datatype 'BYTES'" + ); + assert_eq!( + output.shape, + vec![0], + "Expected 'finish_reason' to have shape [0]" + ); + } + _ => panic!("Unexpected output name: {}", output.name), + } + } + } + + #[rstest] + #[tokio::test] + async fn test_infer_cancellation( + #[with(TestPort::InferCancellation as u16)] service_with_engines: ( + KserveService, + Arc, + Arc, + Arc, + ), + text_input: inference::model_infer_request::InferInputTensor, + ) { + // start server + let _running = RunningService::spawn(service_with_engines.0); + let long_running = service_with_engines.3; + + // create client and send request to unregistered model + let mut client = get_ready_client(TestPort::InferCancellation as u16, 5).await; + + let model_name = "long_running"; + let request = tonic::Request::new(ModelInferRequest { + model_name: model_name.into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![text_input], + ..Default::default() + }); + + assert!( + !long_running.was_cancelled(), + "Expected long running engine is not cancelled" + ); + + // Cancelling the request by dropping the request future after 1 second + let response = match timeout(Duration::from_millis(500), client.model_infer(request)).await + { + Ok(_) => Err("Expect request timed out"), + Err(_) => { + println!("Cancelled request after 500ms"); + Ok("timed out") + } + }; + assert!(response.is_ok(), "Expected client timed out",); + long_running.wait_for_delay().await; + assert!( + long_running.was_cancelled(), + "Expected long running engine to be cancelled" + ); + } + + #[rstest] + #[tokio::test] + async fn test_stream_infer_success( + #[with(TestPort::StreamInferSuccess as u16)] service_with_engines: ( + KserveService, + Arc, + Arc, + Arc, + ), + text_input: inference::model_infer_request::InferInputTensor, + ) { + // start server + let _running = RunningService::spawn(service_with_engines.0); + + // create client and send request to unregistered model + let mut client = get_ready_client(TestPort::StreamInferSuccess as u16, 5).await; + + let model_name = "split"; + let request_id = "1234"; + + // Response streaming true + { + let text_input = text_input.clone(); + let outbound = async_stream::stream! { + let request_count = 1; + for _ in 0..request_count { + let request = ModelInferRequest { + model_name: model_name.into(), + model_version: "1".into(), + id: request_id.into(), + inputs: vec![text_input.clone(), + inference::model_infer_request::InferInputTensor{ + name: "streaming".into(), + datatype: "BOOL".into(), + shape: vec![1], + contents: Some(inference::InferTensorContents { + bool_contents: vec![true], + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + yield request; + } + }; + + let response = client + .model_stream_infer(Request::new(outbound)) + .await + .unwrap(); + let mut inbound = response.into_inner(); + + let mut response_idx = 0; + while let Some(response) = inbound.message().await.unwrap() { + assert!( + response.error_message.is_empty(), + "Expected successful inference" + ); + assert!( + response.infer_response.is_some(), + "Expected successful inference" + ); + + if let Some(response) = &response.infer_response { + assert_eq!( + response.model_name, model_name, + "Expected response of the same model name", + ); + assert_eq!( + response.id, request_id, + "Expected response ID to match request ID" + ); + let expected_output: Vec> = vec!["dummy".into(), "input".into()]; + for output in &response.outputs { + match output.name.as_str() { + "text_output" => { + assert_eq!( + output.datatype, "BYTES", + "Expected 'text_output' to have datatype 'BYTES'" + ); + assert_eq!( + output.shape, + vec![1], + "Expected 'text_output' to have shape [1]" + ); + assert_eq!( + output.contents.as_ref().unwrap().bytes_contents, + vec![expected_output[response_idx].clone()], + "Expected 'text_output' to contain 'dummy input'" + ); + } + "finish_reason" => { + assert_eq!( + output.datatype, "BYTES", + "Expected 'finish_reason' to have datatype 'BYTES'" + ); + assert_eq!( + output.shape, + vec![0], + "Expected 'finish_reason' to have shape [0]" + ); + } + _ => panic!("Unexpected output name: {}", output.name), + } + } + } + response_idx += 1; + } + assert_eq!(response_idx, 2, "Expected 2 responses") + } + + // Response streaming false + { + let text_input = text_input.clone(); + let outbound = async_stream::stream! { + let request_count = 2; + for idx in 0..request_count { + let request = ModelInferRequest { + model_name: model_name.into(), + model_version: "1".into(), + id: format!("{idx}"), + inputs: vec![text_input.clone()], + ..Default::default() + }; + + yield request; + } + }; + + let response = client + .model_stream_infer(Request::new(outbound)) + .await + .unwrap(); + let mut inbound = response.into_inner(); + + let mut response_idx = 0; + while let Some(response) = inbound.message().await.unwrap() { + assert!( + response.error_message.is_empty(), + "Expected successful inference" + ); + assert!( + response.infer_response.is_some(), + "Expected successful inference" + ); + + // Each response is the complete inference + if let Some(response) = &response.infer_response { + assert_eq!( + response.model_name, model_name, + "Expected response of the same model name", + ); + // [gluo NOTE] Here we assume the responses across requests are + // processed in the order of receiving requests, which is not true + // if we improve stream handling in gRPC frontend. Consider: + // time 0: request 0 -> long running -> response 0 (time 5) + // time 1: request 1 -> short running -> response 1 (time 2) + // We expect response 1 to be received before response 0 as their + // requests are independent from each other. + assert_eq!( + response.id, + format!("{response_idx}"), + "Expected response ID to match request ID" + ); + for output in &response.outputs { + match output.name.as_str() { + "text_output" => { + assert_eq!( + output.datatype, "BYTES", + "Expected 'text_output' to have datatype 'BYTES'" + ); + assert_eq!( + output.shape, + vec![1], + "Expected 'text_output' to have shape [1]" + ); + let expected_output: Vec> = vec!["dummyinput".into()]; + assert_eq!( + output.contents.as_ref().unwrap().bytes_contents, + expected_output, + "Expected 'text_output' to contain 'dummyinput'" + ); + } + "finish_reason" => { + assert_eq!( + output.datatype, "BYTES", + "Expected 'finish_reason' to have datatype 'BYTES'" + ); + assert_eq!( + output.shape, + vec![0], + "Expected 'finish_reason' to have shape [0]" + ); + } + _ => panic!("Unexpected output name: {}", output.name), + } + } + } + response_idx += 1; + } + assert_eq!( + response_idx, 2, + "Expected 2 responses, each for one of the two requests" + ) + } + } + + #[rstest] + #[tokio::test] + async fn test_stream_infer_failure( + #[with(TestPort::StreamInferFailure as u16)] service_with_engines: ( + KserveService, + Arc, + Arc, + Arc, + ), + text_input: inference::model_infer_request::InferInputTensor, + ) { + // start server + let _running = RunningService::spawn(service_with_engines.0); + + // create client and send request to unregistered model + let mut client = get_ready_client(TestPort::StreamInferFailure as u16, 5).await; + + let model_name = "failure"; + + let outbound = async_stream::stream! { + let request_count = 1; + for _ in 0..request_count { + let request = ModelInferRequest { + model_name: model_name.into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![text_input.clone()], + ..Default::default() + }; + + yield request; + } + }; + + let response = client + .model_stream_infer(Request::new(outbound)) + .await + .unwrap(); + let mut inbound = response.into_inner(); + + loop { + match inbound.message().await { + Ok(Some(_)) => { + panic!("Expecting failure in the stream"); + } + Err(err) => { + assert_eq!( + err.code(), + tonic::Code::Internal, + "Expected Internal error for streaming, get {}", + err + ); + assert!( + err.message().contains("Failed to generate completions:"), + "Expected error message to contain 'Failed to generate completions:', got: {}", + err.message() + ); + } + Ok(None) => { + // End of stream + break; + } + } + } + } + + #[rstest] + #[tokio::test] + async fn test_stream_infer_cancellation( + #[with(TestPort::StreamInferCancellation as u16)] service_with_engines: ( + KserveService, + Arc, + Arc, + Arc, + ), + text_input: inference::model_infer_request::InferInputTensor, + ) { + // start server + let _running = RunningService::spawn(service_with_engines.0); + let long_running = service_with_engines.3; + + // create client and send request to unregistered model + let mut client = get_ready_client(TestPort::StreamInferCancellation as u16, 5).await; + + let model_name = "long_running"; + let outbound = async_stream::stream! { + let request_count = 1; + for _ in 0..request_count { + let request = ModelInferRequest { + model_name: model_name.into(), + model_version: "1".into(), + id: "1234".into(), + inputs: vec![text_input.clone()], + ..Default::default() + }; + + yield request; + } + }; + + assert!( + !long_running.was_cancelled(), + "Expected long running engine is still running" + ); + + // Cancelling the request by dropping the request future after 1 second + let response = match timeout( + Duration::from_millis(500), + client.model_stream_infer(Request::new(outbound)), + ) + .await + { + Ok(response) => response.unwrap(), + Err(_) => { + panic!("Expected response stream is returned immediately"); + } + }; + std::mem::drop(response); // Drop the response to cancel the stream + + long_running.wait_for_delay().await; + assert!( + long_running.was_cancelled(), + "Expected long running engine to be cancelled" + ); + } + + #[rstest] + #[tokio::test] + async fn test_model_info( + #[with(TestPort::ModelInfo as u16)] service_with_engines: ( + KserveService, + Arc, + Arc, + Arc, + ), + ) { + // start server + let _running = RunningService::spawn(service_with_engines.0); + + // create client and send request to unregistered model + let mut client = get_ready_client(TestPort::ModelInfo as u16, 5).await; + + // Failure unknown_model + let request = tonic::Request::new(ModelMetadataRequest { + name: "Tonic".into(), + version: "".into(), + }); + + let response = client.model_metadata(request).await; + assert!(response.is_err()); + let err = response.unwrap_err(); + assert_eq!( + err.code(), + tonic::Code::NotFound, + "Expected NotFound error for unregistered model, get {}", + err + ); + + let request = tonic::Request::new(ModelConfigRequest { + name: "Tonic".into(), + version: "".into(), + }); + + let response = client.model_config(request).await; + assert!(response.is_err()); + let err = response.unwrap_err(); + assert_eq!( + err.code(), + tonic::Code::NotFound, + "Expected NotFound error for unregistered model, get {}", + err + ); + + // Success metadata + let model_name = "split"; + let request = tonic::Request::new(ModelMetadataRequest { + name: model_name.into(), + version: "1".into(), + }); + + let response = client.model_metadata(request).await.unwrap(); + assert_eq!( + response.get_ref().name, + model_name, + "Expected response of the same model name", + ); + // input + for io in &response.get_ref().inputs { + match io.name.as_str() { + "text_input" => { + assert_eq!( + io.datatype, "BYTES", + "Expected 'text_input' to have datatype 'BYTES'" + ); + assert_eq!( + io.shape, + vec![1], + "Expected 'text_output' to have shape [1]" + ); + } + "streaming" => { + assert_eq!( + io.datatype, "BOOL", + "Expected 'streaming' to have datatype 'BOOL'" + ); + assert_eq!(io.shape, vec![1], "Expected 'streaming' to have shape [1]"); + } + _ => panic!("Unexpected output name: {}", io.name), + } + } + // output + for io in &response.get_ref().outputs { + match io.name.as_str() { + "text_output" => { + assert_eq!( + io.datatype, "BYTES", + "Expected 'text_output' to have datatype 'BYTES'" + ); + assert_eq!( + io.shape, + vec![-1], + "Expected 'text_output' to have shape [-1]" + ); + } + "finish_reason" => { + assert_eq!( + io.datatype, "BYTES", + "Expected 'finish_reason' to have datatype 'BYTES'" + ); + assert_eq!( + io.shape, + vec![-1], + "Expected 'finish_reason' to have shape [-1]" + ); + } + _ => panic!("Unexpected output name: {}", io.name), + } + } + + // success config + let request = tonic::Request::new(ModelConfigRequest { + name: model_name.into(), + version: "1".into(), + }); + + let response = client + .model_config(request) + .await + .unwrap() + .into_inner() + .config; + let Some(config) = response else { + panic!("Expected Some(config), got None"); + }; + assert_eq!( + config.name, model_name, + "Expected response of the same model name", + ); + // input + for io in &config.input { + match io.name.as_str() { + "text_input" => { + assert_eq!( + io.data_type, + DataType::TypeString as i32, + "Expected 'text_input' to have datatype 'TYPE_STRING'" + ); + assert_eq!(io.dims, vec![1], "Expected 'text_output' to have shape [1]"); + } + "streaming" => { + assert_eq!( + io.data_type, + DataType::TypeBool as i32, + "Expected 'streaming' to have datatype 'TYPE_BOOL'" + ); + assert_eq!(io.dims, vec![1], "Expected 'streaming' to have shape [1]"); + } + _ => panic!("Unexpected output name: {}", io.name), + } + } + // output + for io in &config.output { + match io.name.as_str() { + "text_output" => { + assert_eq!( + io.data_type, + DataType::TypeString as i32, + "Expected 'text_output' to have datatype 'TYPE_STRING'" + ); + assert_eq!( + io.dims, + vec![-1], + "Expected 'text_output' to have shape [-1]" + ); + } + "finish_reason" => { + assert_eq!( + io.data_type, + DataType::TypeString as i32, + "Expected 'finish_reason' to have datatype 'TYPE_STRING'" + ); + assert_eq!( + io.dims, + vec![-1], + "Expected 'finish_reason' to have shape [-1]" + ); + } + _ => panic!("Unexpected output name: {}", io.name), + } + } + } +} From 7aa26b5ac71beb9f69f4d6333edb911959fc6ab6 Mon Sep 17 00:00:00 2001 From: Tzu-Ling Kan Date: Wed, 27 Aug 2025 00:36:49 -0700 Subject: [PATCH 47/82] feat: Sglang metrics labels. (#2679) Signed-off-by: Jason Zhou --- .../backends/sglang/src/dynamo/sglang/main.py | 14 +++- .../sglang/src/dynamo/sglang/publisher.py | 18 +++-- tests/serve/test_sglang.py | 77 +++++++++++++++++++ 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/components/backends/sglang/src/dynamo/sglang/main.py b/components/backends/sglang/src/dynamo/sglang/main.py index a56f27fee0..a389bdad62 100644 --- a/components/backends/sglang/src/dynamo/sglang/main.py +++ b/components/backends/sglang/src/dynamo/sglang/main.py @@ -65,7 +65,7 @@ async def init(runtime: DistributedRuntime, config: Config): .client() ) - publisher, metrics_task = await setup_sgl_metrics(engine, component) + publisher, metrics_task, metrics_labels = await setup_sgl_metrics(engine, component) kv_publisher = None if server_args.kv_events_config: @@ -116,7 +116,9 @@ async def register_model(): # Start endpoint immediately and register model concurrently # Requests queue until ready_event is set await asyncio.gather( - generate_endpoint.serve_endpoint(gated_generate, graceful_shutdown=False), + generate_endpoint.serve_endpoint( + handler.generate, graceful_shutdown=False, metrics_labels=metrics_labels + ), register_model(), ) except Exception as e: @@ -146,7 +148,13 @@ async def init_prefill(runtime: DistributedRuntime, config: Config): handler = PrefillWorkerHandler(component, engine, config) - tasks = [generate_endpoint.serve_endpoint(handler.generate, graceful_shutdown=True)] + tasks = [ + generate_endpoint.serve_endpoint( + handler.generate, + graceful_shutdown=True, + metrics_labels=[("model", server_args.served_model_name)], + ) + ] try: await asyncio.gather(*tasks) diff --git a/components/backends/sglang/src/dynamo/sglang/publisher.py b/components/backends/sglang/src/dynamo/sglang/publisher.py index 8acc3f439a..a9a01821d5 100644 --- a/components/backends/sglang/src/dynamo/sglang/publisher.py +++ b/components/backends/sglang/src/dynamo/sglang/publisher.py @@ -3,7 +3,7 @@ import asyncio import logging -from typing import Optional +from typing import List, Optional, Tuple import sglang as sgl import zmq @@ -25,10 +25,15 @@ class DynamoSglangStatPublisher: Handles SGLang metrics reception and publishing. """ - def __init__(self, engine: sgl.Engine, component: Component) -> None: + def __init__( + self, + engine: sgl.Engine, + component: Component, + metrics_labels: Optional[List[Tuple[str, str]]] = None, + ) -> None: self.engine = engine self.inner = WorkerMetricsPublisher() - self.inner.create_endpoint(component) + self.inner.create_endpoint(component, metrics_labels) # Set default values (can be overridden later if needed) self.request_total_slots = 1024 @@ -127,13 +132,14 @@ def record_values( async def setup_sgl_metrics( engine: sgl.Engine, component: Component, -) -> tuple[DynamoSglangStatPublisher, asyncio.Task]: +) -> tuple[DynamoSglangStatPublisher, asyncio.Task, list[tuple[str, str]]]: """ Convenience bootstrap: create endpoint, publish an initial update, and start the metrics loop. """ - publisher = DynamoSglangStatPublisher(engine, component) + metrics_labels = [("model", engine.server_args.served_model_name)] + publisher = DynamoSglangStatPublisher(engine, component, metrics_labels) publisher.init_publish() task = asyncio.create_task(publisher.run()) logging.info("SGLang metrics loop started") - return publisher, task + return publisher, task, metrics_labels diff --git a/tests/serve/test_sglang.py b/tests/serve/test_sglang.py index 2b7e4273a3..1044145235 100644 --- a/tests/serve/test_sglang.py +++ b/tests/serve/test_sglang.py @@ -4,6 +4,7 @@ import logging import os import re +import time from dataclasses import dataclass from typing import Any, List @@ -209,6 +210,82 @@ def test_sglang_deployment(request, runtime_services, sglang_config_test): logger.info(f"SGLang completions response: {text}") +@pytest.mark.e2e +@pytest.mark.gpu_1 +@pytest.mark.sglang +@pytest.mark.slow +def test_metrics_labels(request, runtime_services): + """ + Test that the sglang backend correctly exports model labels in its metrics. + This test verifies that the model name appears as a label in the Prometheus metrics. + """ + logger.info("Starting test_metrics_labels for sglang backend") + + # Configuration + model_path = "Qwen/Qwen3-0.6B" + metrics_port = 8081 + + # Build command to start sglang backend with metrics enabled + command = [ + "python3", + "-m", + "dynamo.sglang", + "--model-path", + model_path, + "--mem-fraction-static", + "0.4", # Limit memory usage for testing + ] + + # Set environment for metrics + env = os.environ.copy() + env["DYN_SYSTEM_ENABLED"] = "true" + env["DYN_SYSTEM_PORT"] = str(metrics_port) + + # Use ManagedProcess for consistent process management + with ManagedProcess( + command=command, + env=env, + timeout=120, + display_output=True, + health_check_urls=[ + (f"http://localhost:{metrics_port}/metrics", lambda r: r.status_code == 200) + ], + delayed_start=30, # Give SGLang time to initialize + ): + # Give the backend a moment to fully initialize metrics + time.sleep(2) + + # Fetch and verify metrics + logger.info("Fetching metrics to verify model label...") + response = requests.get(f"http://localhost:{metrics_port}/metrics", timeout=10) + assert response.status_code == 200, "Failed to fetch metrics" + + metrics_text = response.text + logger.info(f"Metrics text: {metrics_text}") + + # Parse the Prometheus metrics to find our label + pattern = rf'dynamo_component_requests_total\{{[^}}]*model="{re.escape(model_path)}"[^}}]*\}}\s+(\d+)' + matches = re.findall(pattern, metrics_text) + + if matches: + initial_value = int(matches[0]) + assert ( + initial_value == 0 + ), f"Expected initial metric value to be 0, got {initial_value}" + else: + # Check if any dynamo_component metrics exist + if "dynamo_component" in metrics_text: + logger.info( + "✓ Metrics endpoint is working (found dynamo_component metrics)" + ) + logger.warning( + "Note: dynamo_component_requests_total not found - likely because the engine didn't fully initialize" + ) + logger.info("For complete testing, use a real pre-built TRT-LLM engine") + else: + pytest.fail("No dynamo_component metrics found at all") + + @pytest.mark.skip( reason="Requires 4 GPUs - enable when hardware is consistently available" ) From 8b694f18da7471441ed76bca7ccc85674e2ed061 Mon Sep 17 00:00:00 2001 From: atchernych Date: Wed, 27 Aug 2025 11:37:29 -0700 Subject: [PATCH 48/82] chore: Add atchernych to deploy codeowners (#2751) Signed-off-by: Anna Tchernych Signed-off-by: Jason Zhou --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8e0bf4bf7a..fe5a462df4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,7 +14,7 @@ Cargo.toml @ai-dynamo/dynamo-rust-codeowners /container/ @rmccorm4 @tanmayv25 @richardhuo-nv @ptarasiewiczNV @ishandhanani @alec-flowers @nnshah1 @ai-dynamo/Devops # Dynamo deploy -/deploy/ @hutm @biswapanda @ishandhanani @julienmancuso @hhzhang16 @nnshah1 @mohammedabdulwahhab +/deploy/ @hutm @biswapanda @ishandhanani @julienmancuso @hhzhang16 @nnshah1 @mohammedabdulwahhab @atchernych # Examples /examples/ @ai-dynamo/Devops @nnshah1 @whoisj @nealvaidya @ishandhanani @ai-dynamo/dynamo-rust-codeowners From 7da5a38b719bde1507fb671798a9cae64879a695 Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:21:25 -0600 Subject: [PATCH 49/82] fix: Reflect actual status of Grove PGS in Dynamo DGD (#2710) Signed-off-by: Julien Mancuso Signed-off-by: Jason Zhou --- .../dynamocomponentdeployment_types.go | 11 +- deploy/cloud/operator/go.mod | 2 +- deploy/cloud/operator/go.sum | 4 +- .../dynamographdeployment_controller.go | 56 ++++++---- .../internal/controller_common/resource.go | 10 +- .../operator/internal/dynamo/graph_test.go | 2 +- .../cloud/operator/internal/dynamo/grove.go | 100 ++++++++++++++++++ 7 files changed, 153 insertions(+), 32 deletions(-) diff --git a/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go b/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go index 0e99bef761..ae6e233696 100644 --- a/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go +++ b/deploy/cloud/operator/api/v1alpha1/dynamocomponentdeployment_types.go @@ -202,17 +202,18 @@ func init() { SchemeBuilder.Register(&DynamoComponentDeployment{}, &DynamoComponentDeploymentList{}) } -func (s *DynamoComponentDeployment) IsReady() bool { - return s.Status.IsReady() +func (s *DynamoComponentDeployment) IsReady() (bool, string) { + ready, reason := s.Status.IsReady() + return ready, reason } -func (s *DynamoComponentDeploymentStatus) IsReady() bool { +func (s *DynamoComponentDeploymentStatus) IsReady() (bool, string) { for _, condition := range s.Conditions { if condition.Type == DynamoGraphDeploymentConditionTypeAvailable && condition.Status == metav1.ConditionTrue { - return true + return true, "" } } - return false + return false, "Component deployment not ready - Available condition not true" } func (s *DynamoComponentDeployment) GetSpec() any { diff --git a/deploy/cloud/operator/go.mod b/deploy/cloud/operator/go.mod index a3f12d8894..baae50dedd 100644 --- a/deploy/cloud/operator/go.mod +++ b/deploy/cloud/operator/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.3 require ( emperror.dev/errors v0.8.1 - github.com/NVIDIA/grove/operator/api v0.0.0-20250801123021-8b42bac59ef2 + github.com/NVIDIA/grove/operator/api v0.0.0-20250825164137-da01400261a6 github.com/bsm/gomega v1.27.10 github.com/google/go-cmp v0.7.0 github.com/imdario/mergo v0.3.6 diff --git a/deploy/cloud/operator/go.sum b/deploy/cloud/operator/go.sum index 13bd355027..7f01c23e46 100644 --- a/deploy/cloud/operator/go.sum +++ b/deploy/cloud/operator/go.sum @@ -1,7 +1,7 @@ emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= -github.com/NVIDIA/grove/operator/api v0.0.0-20250801123021-8b42bac59ef2 h1:JLOj0GiubP3VlR0okIbuqljvl+e2Vccnu6LX6wL34G0= -github.com/NVIDIA/grove/operator/api v0.0.0-20250801123021-8b42bac59ef2/go.mod h1:QlsR2wQLj9m/zVEqv5SsCPzyjN2ykYZ0r/NEnDf4WB4= +github.com/NVIDIA/grove/operator/api v0.0.0-20250825164137-da01400261a6 h1:JkW8LeRVsQH/YkRTz80T/JxlDgfk0URKgTUKyYKxbso= +github.com/NVIDIA/grove/operator/api v0.0.0-20250825164137-da01400261a6/go.mod h1:QlsR2wQLj9m/zVEqv5SsCPzyjN2ykYZ0r/NEnDf4WB4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= diff --git a/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go b/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go index df1a667a80..3f0d3b8585 100644 --- a/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go +++ b/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go @@ -105,7 +105,6 @@ func (r *DynamoGraphDeploymentReconciler) Reconcile(ctx context.Context, req ctr reason := Reason("undefined") message := Message("") state := PendingState - readyStatus := metav1.ConditionFalse // retrieve the CRD dynamoDeployment := &nvidiacomv1alpha1.DynamoGraphDeployment{} if err = r.Get(ctx, req.NamespacedName, dynamoDeployment); err != nil { @@ -123,10 +122,13 @@ func (r *DynamoGraphDeploymentReconciler) Reconcile(ctx context.Context, req ctr logger.Error(err, "Reconciliation failed") } dynamoDeployment.SetState(string(state)) + + readyStatus := metav1.ConditionFalse if state == ReadyState { readyStatus = metav1.ConditionTrue } - // update the CRD status condition + + // Update Ready condition dynamoDeployment.AddStatusCondition(metav1.Condition{ Type: "Ready", Status: readyStatus, @@ -134,6 +136,7 @@ func (r *DynamoGraphDeploymentReconciler) Reconcile(ctx context.Context, req ctr Message: string(message), LastTransitionTime: metav1.Now(), }) + err = r.Status().Update(ctx, dynamoDeployment) if err != nil { logger.Error(err, "Unable to update the CRD status", "crd", req.NamespacedName) @@ -160,7 +163,7 @@ func (r *DynamoGraphDeploymentReconciler) Reconcile(ctx context.Context, req ctr } type Resource interface { - IsReady() bool + IsReady() (ready bool, reason string) GetName() string } @@ -267,12 +270,17 @@ func (r *DynamoGraphDeploymentReconciler) reconcileGroveResources(ctx context.Co logger.Error(err, "failed to sync the Grove GangSet") return "", "", "", fmt.Errorf("failed to sync the Grove GangSet: %w", err) } - groveGangSetAsResource := commonController.WrapResource(syncedGroveGangSet, func() bool { - if syncedGroveGangSet.Status.LastOperation != nil && syncedGroveGangSet.Status.LastOperation.State == grovev1alpha1.LastOperationStateSucceeded { - return true - } - return false - }) + groveGangSetAsResource := commonController.WrapResource( + syncedGroveGangSet, + func() (bool, string) { + // Grove readiness: all underlying PodCliques and PodCliqueScalingGroups have replicas == availableReplicas + allComponentsReady, reason := dynamo.EvaluateAllComponentsReady(ctx, r.Client, dynamoDeployment) + if !allComponentsReady { + return false, reason + } + return true, "" + }, + ) // Handle Grove scaling operations after structural changes if err := r.reconcileGroveScaling(ctx, dynamoDeployment); err != nil { @@ -296,9 +304,10 @@ func (r *DynamoGraphDeploymentReconciler) reconcileGroveResources(ctx context.Co logger.Error(err, "failed to sync the main component service") return "", "", "", fmt.Errorf("failed to sync the main component service: %w", err) } - mainComponentServiceAsResource := commonController.WrapResource(syncedMainComponentService, func() bool { - return true - }) + mainComponentServiceAsResource := commonController.WrapResource(syncedMainComponentService, + func() (bool, string) { + return true, "" + }) resources = append(resources, mainComponentServiceAsResource) // generate the main component ingress ingressSpec := dynamo.GenerateDefaultIngressSpec(dynamoDeployment, r.Config.IngressConfig) @@ -317,9 +326,10 @@ func (r *DynamoGraphDeploymentReconciler) reconcileGroveResources(ctx context.Co logger.Error(err, "failed to sync the main component ingress") return "", "", "", fmt.Errorf("failed to sync the main component ingress: %w", err) } - resources = append(resources, commonController.WrapResource(syncedMainComponentIngress, func() bool { - return true - })) + resources = append(resources, commonController.WrapResource(syncedMainComponentIngress, + func() (bool, string) { + return true, "" + })) // generate the main component virtual service if r.Config.IngressConfig.UseVirtualService() { mainComponentVirtualService := dynamo.GenerateComponentVirtualService(ctx, dynamo.GetDynamoComponentName(dynamoDeployment, componentName), dynamoDeployment.Namespace, ingressSpec) @@ -334,9 +344,10 @@ func (r *DynamoGraphDeploymentReconciler) reconcileGroveResources(ctx context.Co logger.Error(err, "failed to sync the main component virtual service") return "", "", "", fmt.Errorf("failed to sync the main component virtual service: %w", err) } - resources = append(resources, commonController.WrapResource(syncedMainComponentVirtualService, func() bool { - return true - })) + resources = append(resources, commonController.WrapResource(syncedMainComponentVirtualService, + func() (bool, string) { + return true, "" + })) } } } @@ -344,16 +355,21 @@ func (r *DynamoGraphDeploymentReconciler) reconcileGroveResources(ctx context.Co } func (r *DynamoGraphDeploymentReconciler) checkResourcesReadiness(resources []Resource) (State, Reason, Message, error) { + var notReadyReasons []string notReadyResources := []string{} + for _, resource := range resources { - if !resource.IsReady() { + ready, reason := resource.IsReady() + if !ready { notReadyResources = append(notReadyResources, resource.GetName()) + notReadyReasons = append(notReadyReasons, fmt.Sprintf("%s: %s", resource.GetName(), reason)) } } + if len(notReadyResources) == 0 { return ReadyState, "all_resources_are_ready", Message("All resources are ready"), nil } - return PendingState, "some_resources_are_not_ready", Message(fmt.Sprintf("%d resources not ready: %v", len(notReadyResources), notReadyResources)), nil + return PendingState, "some_resources_are_not_ready", Message(fmt.Sprintf("Resources not ready: %s", strings.Join(notReadyReasons, "; "))), nil } func (r *DynamoGraphDeploymentReconciler) reconcileDynamoComponentsDeployments(ctx context.Context, dynamoDeployment *nvidiacomv1alpha1.DynamoGraphDeployment) (State, Reason, Message, error) { diff --git a/deploy/cloud/operator/internal/controller_common/resource.go b/deploy/cloud/operator/internal/controller_common/resource.go index 4ee824de09..1d3a5a4a98 100644 --- a/deploy/cloud/operator/internal/controller_common/resource.go +++ b/deploy/cloud/operator/internal/controller_common/resource.go @@ -473,16 +473,20 @@ func GetResourcesConfig(resources *common.Resources) (*corev1.ResourceRequiremen type Resource struct { client.Object - isReady func() bool + isReady func() (bool, string) } -func WrapResource[T client.Object](resource T, isReady func() bool) *Resource { +func WrapResource[T client.Object](resource T, isReady func() (bool, string)) *Resource { return &Resource{ Object: resource, isReady: isReady, } } -func (r *Resource) IsReady() bool { +func (r *Resource) IsReady() (bool, string) { return r.isReady() } + +func (r *Resource) GetName() string { + return r.Object.GetName() +} diff --git a/deploy/cloud/operator/internal/dynamo/graph_test.go b/deploy/cloud/operator/internal/dynamo/graph_test.go index 64437f0a28..2584d592ab 100644 --- a/deploy/cloud/operator/internal/dynamo/graph_test.go +++ b/deploy/cloud/operator/internal/dynamo/graph_test.go @@ -4523,7 +4523,7 @@ func TestGenerateBasePodSpec_Worker(t *testing.T) { VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, - SizeLimit: func() *resource.Quantity { q := resource.MustParse("8Gi"); return &q }(), + SizeLimit: func() *resource.Quantity { q := resource.MustParse(commonconsts.DefaultSharedMemorySize); return &q }(), }, }, }, diff --git a/deploy/cloud/operator/internal/dynamo/grove.go b/deploy/cloud/operator/internal/dynamo/grove.go index c130983be2..55fd68e780 100644 --- a/deploy/cloud/operator/internal/dynamo/grove.go +++ b/deploy/cloud/operator/internal/dynamo/grove.go @@ -1,8 +1,18 @@ package dynamo import ( + "context" "fmt" + "strings" + grovev1alpha1 "github.com/NVIDIA/grove/operator/api/core/v1alpha1" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/v1alpha1" commonconsts "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts" ) @@ -30,3 +40,93 @@ func (d *GroveMultinodeDeployer) GetHostNames(serviceName string, numberOfNodes } return hostnames } + +// EvaluateAllComponentsReady determines if all Grove components are ready +// - PodCliques: spec.replicas == status.readyReplicas +// - PodCliqueScalingGroups: spec.replicas == status.availableReplicas +func EvaluateAllComponentsReady(ctx context.Context, client client.Client, dgd *nvidiacomv1alpha1.DynamoGraphDeployment) (bool, string) { + logger := log.FromContext(ctx) + var notReadyComponents []string + + for serviceName, component := range dgd.Spec.Services { + numberOfNodes := component.GetNumberOfNodes() + isMultinode := numberOfNodes > 1 + resourceName := fmt.Sprintf("%s-0-%s", dgd.Name, strings.ToLower(serviceName)) + + if isMultinode { + // Check PodCliqueScalingGroup: spec.replicas == status.availableReplicas + if ok, reason := checkPCSGReady(ctx, client, resourceName, dgd.Namespace, logger); !ok { + notReadyComponents = append(notReadyComponents, fmt.Sprintf("pcsg/%s: %s", resourceName, reason)) + } + } else { + // Check PodClique: spec.replicas == status.readyReplicas + if ok, reason := checkPodCliqueReady(ctx, client, resourceName, dgd.Namespace, logger); !ok { + notReadyComponents = append(notReadyComponents, fmt.Sprintf("podclique/%s: %s", resourceName, reason)) + } + } + } + + if len(notReadyComponents) > 0 { + return false, strings.Join(notReadyComponents, "; ") + } + + return true, "" +} + +// checkPodCliqueReady checks if a PodClique has spec.replicas == status.readyReplicas +func checkPodCliqueReady(ctx context.Context, client client.Client, resourceName, namespace string, logger logr.Logger) (bool, string) { + podClique := &grovev1alpha1.PodClique{} + err := client.Get(ctx, types.NamespacedName{Name: resourceName, Namespace: namespace}, podClique) + if err != nil { + if errors.IsNotFound(err) { + logger.V(2).Info("PodClique not found", "resourceName", resourceName) + return false, "resource not found" + } + logger.V(1).Info("Failed to get PodClique", "error", err, "resourceName", resourceName) + return false, fmt.Sprintf("get error: %v", err) + } + + desiredReplicas := podClique.Spec.Replicas + readyReplicas := podClique.Status.ReadyReplicas + + if desiredReplicas == 0 { + // No replicas desired, so it's ready + return true, "" + } + + if desiredReplicas != readyReplicas { + logger.V(1).Info("PodClique not ready", "resourceName", resourceName, "desired", desiredReplicas, "ready", readyReplicas) + return false, fmt.Sprintf("desired=%d, ready=%d", desiredReplicas, readyReplicas) + } + + return true, "" +} + +// checkPCSGReady checks if a PodCliqueScalingGroup has spec.replicas == status.availableReplicas +func checkPCSGReady(ctx context.Context, client client.Client, resourceName, namespace string, logger logr.Logger) (bool, string) { + pcsg := &grovev1alpha1.PodCliqueScalingGroup{} + err := client.Get(ctx, types.NamespacedName{Name: resourceName, Namespace: namespace}, pcsg) + if err != nil { + if errors.IsNotFound(err) { + logger.V(2).Info("PodCliqueScalingGroup not found", "resourceName", resourceName) + return false, "resource not found" + } + logger.V(1).Info("Failed to get PodCliqueScalingGroup", "error", err, "resourceName", resourceName) + return false, fmt.Sprintf("get error: %v", err) + } + + desiredReplicas := pcsg.Spec.Replicas + availableReplicas := pcsg.Status.AvailableReplicas + + if desiredReplicas == 0 { + // No replicas desired, so it's ready + return true, "" + } + + if desiredReplicas != availableReplicas { + logger.V(1).Info("PodCliqueScalingGroup not ready", "resourceName", resourceName, "desired", desiredReplicas, "available", availableReplicas) + return false, fmt.Sprintf("desired=%d, available=%d", desiredReplicas, availableReplicas) + } + + return true, "" +} From 03fb426a40f26460d5faf7061eeffbcd3025c696 Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:37:00 -0600 Subject: [PATCH 50/82] fix: revisit grove and LWS selection (#2564) Signed-off-by: Julien Mancuso Signed-off-by: Jason Zhou --- deploy/cloud/helm/deploy.sh | 3 +- deploy/cloud/helm/dynamo-platform-values.yaml | 1 - deploy/cloud/helm/platform/Chart.yaml | 4 +-- .../platform/components/operator/Chart.yaml | 4 +-- .../operator/templates/deployment.yaml | 3 -- .../operator/templates/manager-rbac.yaml | 2 -- .../platform/components/operator/values.yaml | 1 - deploy/cloud/helm/platform/values.yaml | 1 - .../v1alpha1/dynamographdeployment_types.go | 10 ++++++ deploy/cloud/operator/cmd/main.go | 12 ++++--- .../dynamocomponentdeployment_controller.go | 4 +-- .../dynamographdeployment_controller.go | 27 +++++++++++---- .../internal/controller_common/predicate.go | 34 +++++++++++++------ 13 files changed, 67 insertions(+), 39 deletions(-) diff --git a/deploy/cloud/helm/deploy.sh b/deploy/cloud/helm/deploy.sh index c562603b85..f9883e6f74 100755 --- a/deploy/cloud/helm/deploy.sh +++ b/deploy/cloud/helm/deploy.sh @@ -48,7 +48,6 @@ export ISTIO_ENABLED="${ISTIO_ENABLED:=false}" export ISTIO_GATEWAY="${ISTIO_GATEWAY:=istio-system/istio-ingressgateway}" export INGRESS_CLASS="${INGRESS_CLASS:=nginx}" export VIRTUAL_SERVICE_SUPPORTS_HTTPS="${VIRTUAL_SERVICE_SUPPORTS_HTTPS:=false}" -export ENABLE_LWS="${ENABLE_LWS:=false}" export DOCKER_REGISTRY_USE_KUBERNETES_SECRET="${DOCKER_REGISTRY_USE_KUBERNETES_SECRET:=false}" # Add command line options @@ -167,7 +166,7 @@ echo "VIRTUAL_SERVICE_SUPPORTS_HTTPS: $VIRTUAL_SERVICE_SUPPORTS_HTTPS" echo "INSTALL_CRDS: $INSTALL_CRDS" echo "DOCKER_REGISTRY_USE_KUBERNETES_SECRET: $DOCKER_REGISTRY_USE_KUBERNETES_SECRET" -envsubst '${NAMESPACE} ${RELEASE_NAME} ${DOCKER_USERNAME} ${DOCKER_PASSWORD} ${DOCKER_SERVER} ${IMAGE_TAG} ${DYNAMO_INGRESS_SUFFIX} ${PIPELINES_DOCKER_SERVER} ${PIPELINES_DOCKER_USERNAME} ${PIPELINES_DOCKER_PASSWORD} ${DOCKER_SECRET_NAME} ${INGRESS_ENABLED} ${ISTIO_ENABLED} ${INGRESS_CLASS} ${ISTIO_GATEWAY} ${VIRTUAL_SERVICE_SUPPORTS_HTTPS} ${ENABLE_LWS} ${DOCKER_REGISTRY_USE_KUBERNETES_SECRET}' < dynamo-platform-values.yaml > generated-values.yaml +envsubst '${NAMESPACE} ${RELEASE_NAME} ${DOCKER_USERNAME} ${DOCKER_PASSWORD} ${DOCKER_SERVER} ${IMAGE_TAG} ${DYNAMO_INGRESS_SUFFIX} ${PIPELINES_DOCKER_SERVER} ${PIPELINES_DOCKER_USERNAME} ${PIPELINES_DOCKER_PASSWORD} ${DOCKER_SECRET_NAME} ${INGRESS_ENABLED} ${ISTIO_ENABLED} ${INGRESS_CLASS} ${ISTIO_GATEWAY} ${VIRTUAL_SERVICE_SUPPORTS_HTTPS} ${DOCKER_REGISTRY_USE_KUBERNETES_SECRET}' < dynamo-platform-values.yaml > generated-values.yaml echo "generated file contents:" cat generated-values.yaml diff --git a/deploy/cloud/helm/dynamo-platform-values.yaml b/deploy/cloud/helm/dynamo-platform-values.yaml index 4f79cb89cf..54fee9e8fe 100644 --- a/deploy/cloud/helm/dynamo-platform-values.yaml +++ b/deploy/cloud/helm/dynamo-platform-values.yaml @@ -23,7 +23,6 @@ dynamo-operator: - name: ${DOCKER_SECRET_NAME} dynamo: - enableLWS: ${ENABLE_LWS} ingress: enabled: ${INGRESS_ENABLED} className: ${INGRESS_CLASS} diff --git a/deploy/cloud/helm/platform/Chart.yaml b/deploy/cloud/helm/platform/Chart.yaml index de2c5e825c..ea00f3189f 100644 --- a/deploy/cloud/helm/platform/Chart.yaml +++ b/deploy/cloud/helm/platform/Chart.yaml @@ -19,11 +19,11 @@ maintainers: url: https://www.nvidia.com description: A Helm chart for NVIDIA Dynamo Platform. type: application -version: 0.4.1 +version: 0.5.0 home: https://nvidia.com dependencies: - name: dynamo-operator - version: 0.4.1 + version: 0.5.0 repository: file://components/operator condition: dynamo-operator.enabled - name: nats diff --git a/deploy/cloud/helm/platform/components/operator/Chart.yaml b/deploy/cloud/helm/platform/components/operator/Chart.yaml index f1337176d7..8e284e9f1d 100644 --- a/deploy/cloud/helm/platform/components/operator/Chart.yaml +++ b/deploy/cloud/helm/platform/components/operator/Chart.yaml @@ -27,9 +27,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.4.1 +version: 0.5.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.4.1" +appVersion: "0.5.0" diff --git a/deploy/cloud/helm/platform/components/operator/templates/deployment.yaml b/deploy/cloud/helm/platform/components/operator/templates/deployment.yaml index 6ea9fb11e9..40f7a0ff16 100644 --- a/deploy/cloud/helm/platform/components/operator/templates/deployment.yaml +++ b/deploy/cloud/helm/platform/components/operator/templates/deployment.yaml @@ -101,9 +101,6 @@ spec: {{- if .Values.dynamo.virtualServiceSupportsHTTPS }} - --virtual-service-supports-https={{ .Values.dynamo.virtualServiceSupportsHTTPS }} {{- end }} - {{- if .Values.dynamo.enableLWS }} - - --enable-lws - {{- end }} {{- if .Values.dynamo.groveTerminationDelay }} - --grove-termination-delay={{ .Values.dynamo.groveTerminationDelay }} {{- end }} diff --git a/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml b/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml index 8864c05bd2..a923fbb365 100644 --- a/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml +++ b/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml @@ -425,7 +425,6 @@ rules: - patch - update - watch -{{- if .Values.dynamo.enableLWS }} - apiGroups: - leaderworkerset.x-k8s.io resources: @@ -450,7 +449,6 @@ rules: - patch - update - watch -{{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 {{- if .Values.namespaceRestriction.enabled }} diff --git a/deploy/cloud/helm/platform/components/operator/values.yaml b/deploy/cloud/helm/platform/components/operator/values.yaml index a4e576e663..7739c50591 100644 --- a/deploy/cloud/helm/platform/components/operator/values.yaml +++ b/deploy/cloud/helm/platform/components/operator/values.yaml @@ -82,7 +82,6 @@ dynamo: serviceAccount: annotations: {} - enableLWS: false groveTerminationDelay: 15m internalImages: diff --git a/deploy/cloud/helm/platform/values.yaml b/deploy/cloud/helm/platform/values.yaml index eacf90302e..fd6cd045a1 100644 --- a/deploy/cloud/helm/platform/values.yaml +++ b/deploy/cloud/helm/platform/values.yaml @@ -34,7 +34,6 @@ dynamo-operator: - --metrics-bind-address=127.0.0.1:8080 imagePullSecrets: [] dynamo: - enableLWS: false groveTerminationDelay: 15m internalImages: debugger: python:3.12-slim diff --git a/deploy/cloud/operator/api/v1alpha1/dynamographdeployment_types.go b/deploy/cloud/operator/api/v1alpha1/dynamographdeployment_types.go index 9b21ea98bc..dc8b428bcb 100644 --- a/deploy/cloud/operator/api/v1alpha1/dynamographdeployment_types.go +++ b/deploy/cloud/operator/api/v1alpha1/dynamographdeployment_types.go @@ -110,3 +110,13 @@ func (s *DynamoGraphDeployment) AddStatusCondition(condition metav1.Condition) { // If no matching condition found, append the new one s.Status.Conditions = append(s.Status.Conditions, condition) } + +// HasAnyMultinodeService reports whether any service in the graph is configured with more than one node. +func (s *DynamoGraphDeployment) HasAnyMultinodeService() bool { + for _, svc := range s.Spec.Services { + if svc != nil && svc.GetNumberOfNodes() > 1 { + return true + } + } + return false +} diff --git a/deploy/cloud/operator/cmd/main.go b/deploy/cloud/operator/cmd/main.go index fded2f2f52..d4bd919177 100644 --- a/deploy/cloud/operator/cmd/main.go +++ b/deploy/cloud/operator/cmd/main.go @@ -129,7 +129,6 @@ func main() { var ingressControllerClassName string var ingressControllerTLSSecretName string var ingressHostSuffix string - var enableLWS bool var groveTerminationDelay time.Duration flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -156,8 +155,6 @@ func main() { "The name of the ingress controller TLS secret to use") flag.StringVar(&ingressHostSuffix, "ingress-host-suffix", "", "The suffix to use for the ingress host") - flag.BoolVar(&enableLWS, "enable-lws", false, - "If set, enable leader worker set") flag.DurationVar(&groveTerminationDelay, "grove-termination-delay", consts.DefaultGroveTerminationDelay, "The termination delay for Grove PodGangSets") opts := zap.Options{ @@ -168,11 +165,13 @@ func main() { ctrlConfig := commonController.Config{ RestrictedNamespace: restrictedNamespace, - EnableLWS: enableLWS, Grove: commonController.GroveConfig{ Enabled: false, // Will be set after Grove discovery TerminationDelay: groveTerminationDelay, }, + LWS: commonController.LWSConfig{ + Enabled: false, // Will be set after LWS discovery + }, EtcdAddress: etcdAddr, NatsAddress: natsAddr, IngressConfig: commonController.IngressConfig{ @@ -240,10 +239,13 @@ func main() { os.Exit(1) } - // Detect Grove availability using discovery client + // Detect orchestrators availability using discovery client setupLog.Info("Detecting Grove availability...") groveEnabled := commonController.DetectGroveAvailability(mainCtx, mgr) ctrlConfig.Grove.Enabled = groveEnabled + setupLog.Info("Detecting LWS availability...") + lwsEnabled := commonController.DetectLWSAvailability(mainCtx, mgr) + ctrlConfig.LWS.Enabled = lwsEnabled // Create etcd client cli, err := clientv3.New(clientv3.Config{ diff --git a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go index 455f5da6f4..b2392d6fe5 100644 --- a/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go +++ b/deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go @@ -199,7 +199,7 @@ func (r *DynamoComponentDeploymentReconciler) Reconcile(ctx context.Context, req // Create the appropriate workload resource based on deployment type var leaderWorkerSets []*leaderworkersetv1.LeaderWorkerSet var deployment *appsv1.Deployment - if r.Config.EnableLWS && dynamoComponentDeployment.IsMultinode() { + if r.Config.LWS.Enabled && dynamoComponentDeployment.IsMultinode() { desiredReplicas := int32(1) if dynamoComponentDeployment.Spec.Replicas != nil { desiredReplicas = *dynamoComponentDeployment.Spec.Replicas @@ -1356,7 +1356,7 @@ func (r *DynamoComponentDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) Owns(&corev1.PersistentVolumeClaim{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). WithEventFilter(controller_common.EphemeralDeploymentEventFilter(r.Config)) - if r.Config.EnableLWS { + if r.Config.LWS.Enabled { m.Owns(&leaderworkersetv1.LeaderWorkerSet{}, builder.WithPredicates(predicate.Funcs{ // ignore creation cause we don't want to be called again after we create the LeaderWorkerSet CreateFunc: func(ce event.CreateEvent) bool { return false }, diff --git a/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go b/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go index 3f0d3b8585..afc2b6efb1 100644 --- a/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go +++ b/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go @@ -169,14 +169,27 @@ type Resource interface { func (r *DynamoGraphDeploymentReconciler) reconcileResources(ctx context.Context, dynamoDeployment *nvidiacomv1alpha1.DynamoGraphDeployment) (State, Reason, Message, error) { logger := log.FromContext(ctx) - if r.Config.Grove.Enabled { - // check if explicit opt out of grove - if dynamoDeployment.Annotations[consts.KubeAnnotationEnableGrove] == consts.KubeLabelValueFalse { - logger.Info("Grove is explicitly disabled for this deployment, skipping grove resources reconciliation") - return r.reconcileDynamoComponentsDeployments(ctx, dynamoDeployment) - } + // Orchestrator selection via single boolean annotation: nvidia.com/enable-grove + // Unset or not "false": Grove if available; else component mode + // "false": component mode (multinode -> LWS; single-node -> standard) + enableGrove := true + if dynamoDeployment.Annotations != nil && strings.ToLower(dynamoDeployment.Annotations[consts.KubeAnnotationEnableGrove]) == consts.KubeLabelValueFalse { + enableGrove = false + } + + // Determine if any service is multinode + hasMultinode := dynamoDeployment.HasAnyMultinodeService() + + if enableGrove && r.Config.Grove.Enabled { + logger.Info("Reconciling Grove resources", "enableGrove", enableGrove, "groveEnabled", r.Config.Grove.Enabled, "hasMultinode", hasMultinode, "lwsEnabled", r.Config.LWS.Enabled) return r.reconcileGroveResources(ctx, dynamoDeployment) } + if hasMultinode && !r.Config.LWS.Enabled { + err := fmt.Errorf("no multinode orchestrator available") + logger.Error(err, err.Error(), "hasMultinode", hasMultinode, "lwsEnabled", r.Config.LWS.Enabled, "enableGrove", enableGrove, "groveEnabled", r.Config.Grove.Enabled) + return "", "", "", err + } + logger.Info("Reconciling Dynamo components deployments", "hasMultinode", hasMultinode, "lwsEnabled", r.Config.LWS.Enabled, "enableGrove", enableGrove, "groveEnabled", r.Config.Grove.Enabled) return r.reconcileDynamoComponentsDeployments(ctx, dynamoDeployment) } @@ -285,7 +298,7 @@ func (r *DynamoGraphDeploymentReconciler) reconcileGroveResources(ctx context.Co // Handle Grove scaling operations after structural changes if err := r.reconcileGroveScaling(ctx, dynamoDeployment); err != nil { logger.Error(err, "failed to reconcile Grove scaling") - return FailedState, "grove_scaling_failed", Message(err.Error()), err + return "", "", "", fmt.Errorf("failed to reconcile Grove scaling: %w", err) } resources := []Resource{groveGangSetAsResource} diff --git a/deploy/cloud/operator/internal/controller_common/predicate.go b/deploy/cloud/operator/internal/controller_common/predicate.go index 5ad7724cfb..ab65239396 100644 --- a/deploy/cloud/operator/internal/controller_common/predicate.go +++ b/deploy/cloud/operator/internal/controller_common/predicate.go @@ -37,11 +37,16 @@ type GroveConfig struct { TerminationDelay time.Duration } +type LWSConfig struct { + // Enabled is automatically determined by checking if LWS CRDs are installed in the cluster + Enabled bool +} + type Config struct { // Enable resources filtering, only the resources belonging to the given namespace will be handled. RestrictedNamespace string - EnableLWS bool Grove GroveConfig + LWS LWSConfig EtcdAddress string NatsAddress string IngressConfig IngressConfig @@ -61,40 +66,47 @@ func (i *IngressConfig) UseVirtualService() bool { // DetectGroveAvailability checks if Grove is available by checking if the Grove API group is registered // This approach uses the discovery client which is simpler and more reliable func DetectGroveAvailability(ctx context.Context, mgr ctrl.Manager) bool { + return detectAPIGroupAvailability(ctx, mgr, "grove.io") +} + +// DetectLWSAvailability checks if LWS is available by checking if the LWS API group is registered +// This approach uses the discovery client which is simpler and more reliable +func DetectLWSAvailability(ctx context.Context, mgr ctrl.Manager) bool { + return detectAPIGroupAvailability(ctx, mgr, "leaderworkerset.x-k8s.io") +} + +// detectAPIGroupAvailability checks if a specific API group is registered in the cluster +func detectAPIGroupAvailability(ctx context.Context, mgr ctrl.Manager, groupName string) bool { logger := log.FromContext(ctx) - // Use the discovery client to check if Grove API groups are available cfg := mgr.GetConfig() if cfg == nil { - logger.Info("Grove detection failed, no discovery client available") + logger.Info("detection failed, no discovery client available", "group", groupName) return false } - // Try to create a discovery client discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg) if err != nil { - logger.Error(err, "Grove detection failed, could not create discovery client") + logger.Error(err, "detection failed, could not create discovery client", "group", groupName) return false } - // Check if grove.io API group is available apiGroups, err := discoveryClient.ServerGroups() if err != nil { - logger.Error(err, "Grove detection failed, could not list server groups") + logger.Error(err, "detection failed, could not list server groups", "group", groupName) return false } for _, group := range apiGroups.Groups { - if group.Name == "grove.io" { - logger.Info("Grove is available, grove.io API group found") + if group.Name == groupName { + logger.Info("API group is available", "group", groupName) return true } } - logger.Info("Grove not available, grove.io API group not found") + logger.Info("API group not available", "group", groupName) return false } - func EphemeralDeploymentEventFilter(config Config) predicate.Predicate { return predicate.NewPredicateFuncs(func(o client.Object) bool { l := log.FromContext(context.Background()) From 66ae1677a114493ec21f3b30c47815c5626d9b27 Mon Sep 17 00:00:00 2001 From: mohammedabdulwahhab Date: Wed, 27 Aug 2025 17:08:47 -0700 Subject: [PATCH 51/82] feat: add reference setup for dynamo logging in k8s with loki (#2699) Signed-off-by: mohammedabdulwahhab Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jason Zhou --- .../backends/sglang/deploy/agg_logging.yaml | 43 ++++ deploy/logging/README.md | 3 + deploy/logging/grafana/dashboard.json | 211 +++++++++++++++++ deploy/logging/grafana/logging-dashboard.yaml | 221 ++++++++++++++++++ deploy/logging/grafana/loki-datasource.yaml | 18 ++ deploy/logging/values/alloy-values.yaml | 60 +++++ deploy/logging/values/loki-values.yaml | 79 +++++++ deploy/metrics/k8s/README.md | 2 +- docs/guides/dynamo_deploy/dynamo_cloud.md | 2 +- docs/guides/dynamo_deploy/logging.md | 144 ++++++++++++ .../{k8s_metrics.md => metrics.md} | 0 docs/guides/metrics.md | 2 +- docs/index.rst | 3 +- 13 files changed, 784 insertions(+), 4 deletions(-) create mode 100644 components/backends/sglang/deploy/agg_logging.yaml create mode 100644 deploy/logging/README.md create mode 100644 deploy/logging/grafana/dashboard.json create mode 100644 deploy/logging/grafana/logging-dashboard.yaml create mode 100644 deploy/logging/grafana/loki-datasource.yaml create mode 100644 deploy/logging/values/alloy-values.yaml create mode 100644 deploy/logging/values/loki-values.yaml create mode 100644 docs/guides/dynamo_deploy/logging.md rename docs/guides/dynamo_deploy/{k8s_metrics.md => metrics.md} (100%) diff --git a/components/backends/sglang/deploy/agg_logging.yaml b/components/backends/sglang/deploy/agg_logging.yaml new file mode 100644 index 0000000000..a01142e531 --- /dev/null +++ b/components/backends/sglang/deploy/agg_logging.yaml @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: nvidia.com/v1alpha1 +kind: DynamoGraphDeployment +metadata: + name: sglang-agg +spec: + envs: + - name: DYN_LOGGING_JSONL + value: "1" + services: + Frontend: + dynamoNamespace: sglang-agg + componentType: frontend + replicas: 1 + extraPodSpec: + mainContainer: + image: my-registry/sglang-runtime:my-tag + SGLangDecodeWorker: + envFromSecret: hf-token-secret + dynamoNamespace: sglang-agg + componentType: worker + replicas: 1 + resources: + limits: + gpu: "1" + extraPodSpec: + mainContainer: + image: my-registry/sglang-runtime:my-tag + workingDir: /workspace/components/backends/sglang + command: + - /bin/sh + - -c + args: + - >- + python3 -m dynamo.sglang + --model-path Qwen/Qwen3-0.6B + --served-model-name Qwen/Qwen3-0.6B + --page-size 16 + --tp 1 + --trust-remote-code + --skip-tokenizer-init \ No newline at end of file diff --git a/deploy/logging/README.md b/deploy/logging/README.md new file mode 100644 index 0000000000..99ce31717c --- /dev/null +++ b/deploy/logging/README.md @@ -0,0 +1,3 @@ +# Dynamo Logging on Kubernetes + +For detailed documentation on collecting and visualizing logs on Kubernetes, see [docs/guides/dynamo_deploy/logging.md](../../docs/guides/dynamo_deploy/logging.md). diff --git a/deploy/logging/grafana/dashboard.json b/deploy/logging/grafana/dashboard.json new file mode 100644 index 0000000000..8e8781d6cb --- /dev/null +++ b/deploy/logging/grafana/dashboard.json @@ -0,0 +1,211 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 21, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "dedupStrategy": "none", + "enableInfiniteScrolling": false, + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "direction": "backward", + "editorMode": "builder", + "expr": "{namespace=~\"$namespace\", nvidia_com_dynamo_graph_deployment_name=~\"$dynamographdeployment\", nvidia_com_dynamo_component_type=~\"$component\"} |~ \"(?i)$search\" |~ \"(?i)$trace_id\"", + "queryType": "range", + "refId": "A" + } + ], + "title": "DynamoGraph Logs", + "type": "logs" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": ["dynamograph", "logs"], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "Loki", + "value": "Loki" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "loki", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": "label_values(namespace)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(nvidia_com_dynamo_graph_deployment_name)", + "hide": 0, + "includeAll": true, + "label": "DynamoGraph Deployment", + "multi": true, + "name": "dynamographdeployment", + "options": [], + "query": "label_values(nvidia_com_dynamo_graph_deployment_name)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(nvidia_com_dynamo_component_type)", + "hide": 0, + "includeAll": true, + "label": "Component", + "multi": true, + "name": "component", + "options": [], + "query": "label_values(nvidia_com_dynamo_component_type)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "", + "value": "" + }, + "label": "Trace ID", + "name": "trace_id", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + }, + { + "current": { + "selected": true, + "text": "", + "value": "" + }, + "label": "Search", + "name": "search", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "DynamoGraph Logs", + "description": "Dashboard for viewing DynamoGraph deployment logs across components and namespaces", + "version": 1 + } diff --git a/deploy/logging/grafana/logging-dashboard.yaml b/deploy/logging/grafana/logging-dashboard.yaml new file mode 100644 index 0000000000..52ff87ad72 --- /dev/null +++ b/deploy/logging/grafana/logging-dashboard.yaml @@ -0,0 +1,221 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +apiVersion: v1 +kind: ConfigMap +metadata: + name: dynamo-logs-dashboard + labels: + grafana_dashboard: "1" # This label is important for the Grafana sidecar +data: + dynamo-logs.json: |- + { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 21, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "dedupStrategy": "none", + "enableInfiniteScrolling": false, + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "direction": "backward", + "editorMode": "builder", + "expr": "{namespace=~\"$namespace\", nvidia_com_dynamo_graph_deployment_name=~\"$dynamographdeployment\", nvidia_com_dynamo_component_type=~\"$component\"} |~ \"(?i)$search\" |~ \"(?i)$trace_id\"", + "queryType": "range", + "refId": "A" + } + ], + "title": "DynamoGraph Logs", + "type": "logs" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": ["dynamograph", "logs"], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "Loki", + "value": "Loki" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "loki", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": "label_values(namespace)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(nvidia_com_dynamo_graph_deployment_name)", + "hide": 0, + "includeAll": true, + "label": "DynamoGraph Deployment", + "multi": true, + "name": "dynamographdeployment", + "options": [], + "query": "label_values(nvidia_com_dynamo_graph_deployment_name)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": ["All"], + "value": ["$__all"] + }, + "datasource": { + "type": "loki", + "uid": "$datasource" + }, + "definition": "label_values(nvidia_com_dynamo_component_type)", + "hide": 0, + "includeAll": true, + "label": "Component", + "multi": true, + "name": "component", + "options": [], + "query": "label_values(nvidia_com_dynamo_component_type)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "", + "value": "" + }, + "label": "Trace ID", + "name": "trace_id", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + }, + { + "current": { + "selected": true, + "text": "", + "value": "" + }, + "label": "Search", + "name": "search", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "DynamoGraph Logs", + "description": "Dashboard for viewing DynamoGraph deployment logs across components and namespaces", + "version": 1 + } diff --git a/deploy/logging/grafana/loki-datasource.yaml b/deploy/logging/grafana/loki-datasource.yaml new file mode 100644 index 0000000000..2b15310ff4 --- /dev/null +++ b/deploy/logging/grafana/loki-datasource.yaml @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +apiVersion: v1 +kind: ConfigMap +metadata: + name: loki-datasource + labels: + grafana_datasource: "1" # This label is important for the Grafana sidecar +data: + loki-datasource.yaml: |- + apiVersion: 1 + datasources: + - name: Loki + type: loki + access: proxy + url: http://loki-gateway.$MONITORING_NAMESPACE.svc.cluster.local + jsonData: + maxLines: 1000 diff --git a/deploy/logging/values/alloy-values.yaml b/deploy/logging/values/alloy-values.yaml new file mode 100644 index 0000000000..4c1e7b9c67 --- /dev/null +++ b/deploy/logging/values/alloy-values.yaml @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +cluster: + name: dynamo-log-collector + +destinations: + - name: loki + type: loki + url: http://loki-gateway.$MONITORING_NAMESPACE.svc.cluster.local/loki/api/v1/push + +nodeLogs: + enabled: false + +podLogs: + enabled: true + gatherMethod: kubernetesApi + collector: alloy-logs + labels: + app_kubernetes_io_name: app.kubernetes.io/name + nvidia_com_dynamo_component_type: nvidia.com/dynamo-component-type + nvidia_com_dynamo_graph_deployment_name: nvidia.com/dynamo-graph-deployment-name + labelsToKeep: + - "app_kubernetes_io_name" + - "container" + - "instance" + - "job" + - "level" + - "namespace" + - "service_name" + - "service_namespace" + - "deployment_environment" + - "deployment_environment_name" + - "nvidia_com_dynamo_component_type" + - "nvidia_com_dynamo_graph_deployment_name" + structuredMetadata: + pod: pod # Set structured metadata "pod" from label "pod" + namespaces: + - $DYNAMO_NAMESPACE + +# Collectors +alloy-singleton: + enabled: false + +alloy-metrics: + enabled: false + +alloy-logs: + enabled: true + alloy: + mounts: + varlog: false + dockercontainers: false + clustering: + enabled: true + +alloy-profiles: + enabled: false + +alloy-receiver: + enabled: false \ No newline at end of file diff --git a/deploy/logging/values/loki-values.yaml b/deploy/logging/values/loki-values.yaml new file mode 100644 index 0000000000..9a39db434b --- /dev/null +++ b/deploy/logging/values/loki-values.yaml @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +loki: + auth_enabled: false + commonConfig: + replication_factor: 1 + schemaConfig: + configs: + - from: 2024-04-01 + store: tsdb + object_store: s3 + schema: v13 + index: + prefix: loki_index_ + period: 24h + ingester: + chunk_encoding: snappy + tracing: + enabled: true + pattern_ingester: + enabled: true + limits_config: + allow_structured_metadata: true + volume_enabled: true + ruler: + enable_api: true + querier: + # Default is 4, if you have enough memory and CPU you can increase, reduce if OOMing + max_concurrent: 4 + +minio: + enabled: true + +deploymentMode: SingleBinary +singleBinary: + replicas: 1 + resources: + limits: + cpu: 4 + memory: 4Gi + requests: + cpu: 2 + memory: 2Gi + extraEnv: + # Keep a little bit lower than memory limits + - name: GOMEMLIMIT + value: 3750MiB + +chunksCache: + # default is 500MB, with limited memory keep this smaller + writebackSizeLimit: 10MB + + +# Zero out replica counts of other deployment modes +backend: + replicas: 0 +read: + replicas: 0 +write: + replicas: 0 + +ingester: + replicas: 0 +querier: + replicas: 0 +queryFrontend: + replicas: 0 +queryScheduler: + replicas: 0 +distributor: + replicas: 0 +compactor: + replicas: 0 +indexGateway: + replicas: 0 +bloomCompactor: + replicas: 0 +bloomGateway: + replicas: 0 diff --git a/deploy/metrics/k8s/README.md b/deploy/metrics/k8s/README.md index 0e1fcb6368..ae13722eb5 100644 --- a/deploy/metrics/k8s/README.md +++ b/deploy/metrics/k8s/README.md @@ -1,3 +1,3 @@ # Dynamo Metrics Collection on Kubernetes -For detailed documentation on collecting and visualizing metrics on Kubernetes, see [docs/guides/dynamo_deploy/k8s_metrics.md](../../../docs/guides/dynamo_deploy/k8s_metrics.md). +For detailed documentation on collecting and visualizing metrics on Kubernetes, see [docs/guides/dynamo_deploy/metrics.md](../../../docs/guides/dynamo_deploy/metrics.md). diff --git a/docs/guides/dynamo_deploy/dynamo_cloud.md b/docs/guides/dynamo_deploy/dynamo_cloud.md index 4cc3753024..c45a66a0b7 100644 --- a/docs/guides/dynamo_deploy/dynamo_cloud.md +++ b/docs/guides/dynamo_deploy/dynamo_cloud.md @@ -151,7 +151,7 @@ kubectl get pods -n ${NAMESPACE} - [TensorRT-LLM Deployments](../../../components/backends/trtllm/deploy/README.md) 3. **Optional:** - - [Set up Prometheus & Grafana](k8s_metrics.md) + - [Set up Prometheus & Grafana](metrics.md) - [SLA Planner Deployment Guide](sla_planner_deployment.md) (for advanced SLA-aware scheduling and autoscaling) ## Troubleshooting diff --git a/docs/guides/dynamo_deploy/logging.md b/docs/guides/dynamo_deploy/logging.md new file mode 100644 index 0000000000..cf8e8ed054 --- /dev/null +++ b/docs/guides/dynamo_deploy/logging.md @@ -0,0 +1,144 @@ +# Log Aggregation in Dynamo on Kubernetes + +This guide demonstrates how to set up logging for Dynamo in Kubernetes using Grafana Loki and Alloy. This setup provides a simple reference logging setup that can be followed in Kubernetes clusters including Minikube and MicroK8s. + +> [!Note] +> This setup is intended for development and testing purposes. For production environments, please refer to the official documentation for high-availability configurations. + +## Components Overview + +- **[Grafana Loki](https://grafana.com/oss/loki/)**: Fast and cost-effective Kubernetes-native log aggregation system. + +- **[Grafana Alloy](https://grafana.com/oss/alloy/)**: OpenTelemetry collector that replaces Promtail, gathering logs, metrics and traces from Kubernetes pods. + +- **[Grafana](https://grafana.com/grafana/)**: Visualization platform for querying and exploring logs. + +## Prerequisites + +### 1. Dynamo Cloud Kubernetes Operator + +This guide assumes you have installed Dynamo Cloud Kubernetes Operator. For more information, see [Dynamo Cloud Operator](./README.md). + +### 2. Kube-prometheus + +While this guide does not use Prometheus, it assumes Grafana is pre-installed with the kube-prometheus. For more information, see [kube-prometheus](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack). + +### 3. Environment Variables + +The following env variables are set: +- `MONITORING_NAMESPACE`: The namespace where Loki is installed +- `DYNAMO_NAMESPACE`: The namespace where Dynamo Cloud Operator is installed + +```bash +export MONITORING_NAMESPACE=monitoring +export DYNAMO_NAMESPACE=dynamo-cloud +``` + +## Installation Steps + +### 1. Install Loki + +First, we'll install Loki in single binary mode, which is ideal for testing and development: + +```bash +# Add the Grafana Helm repository +helm repo add grafana https://grafana.github.io/helm-charts +helm repo update + +# Install Loki +helm install --values deploy/logging/values/loki-values.yaml loki grafana/loki -n $MONITORING_NAMESPACE +``` + +Our configuration (`loki-values.yaml`) sets up Loki in a simple configuration that is suitable for testing and development. It uses a local MinIO for storage. The installation pods can be viewed with: +```bash +kubectl get pods -n $MONITORING_NAMESPACE -l app=loki +``` + +### 2. Install Grafana Alloy + +Next, install the Grafana Alloy collector to gather logs from your Kubernetes cluster and forward them to Loki. Here we use the Helm chart `k8s-monitoring` provided by Grafana to install the collector: + +```bash +# Generate a custom values file with the namespace information +envsubst < deploy/logging/values/alloy-values.yaml > alloy-custom-values.yaml + +# Install the collector +helm install --values alloy-custom-values.yaml alloy grafana/k8s-monitoring -n $MONITORING_NAMESPACE +``` + +The values file (`alloy-values.yaml`) includes the following configurations for the collector: +- Destination to forward logs to Loki +- Namespace to collect logs from +- Pod labels to be mapped to Loki labels +- Collection method (kubernetesApi or tailing `/var/log/containers/`) + +```yaml +destinations: +- name: loki + type: loki + url: http://loki-gateway.$MONITORING_NAMESPACE.svc.cluster.local/loki/api/v1/push +podLogs: + enabled: true + gatherMethod: kubernetesApi # collect logs from the kubernetes api, rather than /var/log/containers/; friendly for testing and development + collector: alloy-logs + labels: + app_kubernetes_io_name: app.kubernetes.io/name + nvidia_com_dynamo_component_type: nvidia.com/dynamo-component-type + nvidia_com_dynamo_graph_deployment_name: nvidia.com/dynamo-graph-deployment-name + labelsToKeep: + - "app_kubernetes_io_name" + - "container" + - "instance" + - "job" + - "level" + - "namespace" + - "service_name" + - "service_namespace" + - "deployment_environment" + - "deployment_environment_name" + - "nvidia_com_dynamo_component_type" # extract this label from the dynamo graph deployment + - "nvidia_com_dynamo_graph_deployment_name" # extract this label from the dynamo graph deployment + namespaces: + - $DYNAMO_NAMESPACE +``` + +### 3. Configure Grafana with the Loki datasource and Dynamo Logs dashboard + +We will be viewing the logs associated with our DynamoGraphDeployment in Grafana. To do this, we need to configure Grafana with the Loki datasource and Dynamo Logs dashboard. + +Since we are using Grafana with the Prometheus Operator, we can simply apply the following ConfigMaps to quickly achieve this configuration. + +```bash +# Configure Grafana with the Loki datasource +envsubst < deploy/logging/grafana/loki-datasource.yaml | kubectl apply -n $MONITORING_NAMESPACE -f - + +# Configure Grafana with the Dynamo Logs dashboard +envsubst < deploy/logging/grafana/logging-dashboard.yaml | kubectl apply -n $MONITORING_NAMESPACE -f - +``` + +> [!Note] +> If using Grafana installed without the Prometheus Operator, you can manually import the Loki datasource and Dynamo Logs dashboard using the Grafana UI. + +### 4. Deploy a DynamoGraphDeployment with JSONL Logging + +At this point, we should have everything in place to collect and view logs in our Grafana instance. All that is left is to deploy a DynamoGraphDeployment to collect logs from. + +To enable structured logs in a DynamoGraphDeployment, we need to set the `DYN_LOGGING_JSONL` environment variable to `1`. This is done for us in the `agg_logging.yaml` setup for the Sglang backend. We can now deploy the DynamoGraphDeployment with: + +```bash +kubectl apply -n $DYNAMO_NAMESPACE -f components/backends/sglang/deploy/agg_logging.yaml +``` + +Send a few chat completions requests to generate structured logs across the frontend and worker pods across the DynamoGraphDeployment. We are now all set to view the logs in Grafana. + +## Viewing Logs in Grafana + +Port-forward the Grafana service to access the UI: + +```bash +kubectl port-forward svc/prometheus-grafana 3000:80 -n $MONITORING_NAMESPACE +``` + +If everything is working, under Home > Dashboards > Dynamo Logs, you should see a dashboard that can be used to view the logs associated with our DynamoGraphDeployments + +The dashboard enables filtering by DynamoGraphDeployment, namespace, and component type (e.g frontend, worker, etc). \ No newline at end of file diff --git a/docs/guides/dynamo_deploy/k8s_metrics.md b/docs/guides/dynamo_deploy/metrics.md similarity index 100% rename from docs/guides/dynamo_deploy/k8s_metrics.md rename to docs/guides/dynamo_deploy/metrics.md diff --git a/docs/guides/metrics.md b/docs/guides/metrics.md index 31535ed0db..73699777d3 100644 --- a/docs/guides/metrics.md +++ b/docs/guides/metrics.md @@ -31,7 +31,7 @@ Dynamo automatically exposes metrics with the `dynamo_` name prefixes. It also a **Specialized Component Metrics**: Components can also expose additional metrics specific to their functionality. For example, a `preprocessor` component exposes metrics with the `dynamo_preprocessor_*` prefix. See the [Available Metrics section](../../deploy/metrics/README.md#available-metrics) for details on specialized component metrics. -**Kubernetes Integration**: For comprehensive Kubernetes deployment and monitoring setup, see the [Kubernetes Metrics Guide](dynamo_deploy/k8s_metrics.md). This includes Prometheus Operator setup, metrics collection configuration, and visualization in Grafana. +**Kubernetes Integration**: For comprehensive Kubernetes deployment and monitoring setup, see the [Kubernetes Metrics Guide](dynamo_deploy/metrics.md). This includes Prometheus Operator setup, metrics collection configuration, and visualization in Grafana. ## Metrics Hierarchy diff --git a/docs/index.rst b/docs/index.rst index daac85fdbd..1510db0c5a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -52,7 +52,8 @@ Quickstart Quickstart (K8s) <../guides/dynamo_deploy/dynamo_cloud.md> Dynamo Operator <../guides/dynamo_deploy/dynamo_operator.md> - Metrics <../guides/dynamo_deploy/k8s_metrics.md> + Metrics <../guides/dynamo_deploy/metrics.md> + Logging <../guides/dynamo_deploy/logging.md> Multinode <../guides/dynamo_deploy/multinode-deployment.md> Minikube Setup <../guides/dynamo_deploy/minikube.md> From 7a9a393d2bfef8d69a1e46c476728ac309559637 Mon Sep 17 00:00:00 2001 From: julienmancuso <161955438+julienmancuso@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:09:14 -0600 Subject: [PATCH 52/82] feat: Auto-inject kai-scheduler annotations and label (#2748) Signed-off-by: Julien Mancuso Signed-off-by: Jason Zhou --- .../operator/templates/manager-rbac.yaml | 46 +++ deploy/cloud/operator/cmd/main.go | 8 + deploy/cloud/operator/config/rbac/role.yaml | 7 + .../cloud/operator/internal/consts/consts.go | 34 +- .../dynamographdeployment_controller.go | 19 +- .../internal/controller_common/predicate.go | 13 + .../cloud/operator/internal/dynamo/graph.go | 15 + .../cloud/operator/internal/dynamo/grove.go | 96 ++++++ .../operator/internal/dynamo/grove_test.go | 313 ++++++++++++++++++ .../dynamo_deploy/multinode-deployment.md | 113 +++++-- 10 files changed, 628 insertions(+), 36 deletions(-) create mode 100644 deploy/cloud/operator/internal/dynamo/grove_test.go diff --git a/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml b/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml index a923fbb365..a225b52b13 100644 --- a/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml +++ b/deploy/cloud/helm/platform/components/operator/templates/manager-rbac.yaml @@ -137,6 +137,13 @@ rules: - get - patch - update +- apiGroups: + - scheduling.run.ai + resources: + - queues + verbs: + - get + - list - apiGroups: - apps resources: @@ -475,6 +482,45 @@ roleRef: {{- end }} name: '{{ include "dynamo-operator.fullname" . }}-manager-role' subjects: +- kind: ServiceAccount + name: '{{ include "dynamo-operator.fullname" . }}-controller-manager' + namespace: '{{ .Release.Namespace }}' +--- +# ClusterRole for kai-scheduler queue access +# This is always a ClusterRole since Queue resources are cluster-scoped +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "dynamo-operator.fullname" . }}-queue-reader + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: dynamo-operator + app.kubernetes.io/part-of: dynamo-operator + {{- include "dynamo-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - scheduling.run.ai + resources: + - queues + verbs: + - get + - list +--- +# ClusterRoleBinding for kai-scheduler queue access +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "dynamo-operator.fullname" . }}-queue-reader-binding + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: dynamo-operator + app.kubernetes.io/part-of: dynamo-operator + {{- include "dynamo-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "dynamo-operator.fullname" . }}-queue-reader +subjects: - kind: ServiceAccount name: '{{ include "dynamo-operator.fullname" . }}-controller-manager' namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/deploy/cloud/operator/cmd/main.go b/deploy/cloud/operator/cmd/main.go index d4bd919177..50fd4f60f6 100644 --- a/deploy/cloud/operator/cmd/main.go +++ b/deploy/cloud/operator/cmd/main.go @@ -172,6 +172,9 @@ func main() { LWS: commonController.LWSConfig{ Enabled: false, // Will be set after LWS discovery }, + KaiScheduler: commonController.KaiSchedulerConfig{ + Enabled: false, // Will be set after Kai-scheduler discovery + }, EtcdAddress: etcdAddr, NatsAddress: natsAddr, IngressConfig: commonController.IngressConfig{ @@ -247,6 +250,11 @@ func main() { lwsEnabled := commonController.DetectLWSAvailability(mainCtx, mgr) ctrlConfig.LWS.Enabled = lwsEnabled + // Detect Kai-scheduler availability using discovery client + setupLog.Info("Detecting Kai-scheduler availability...") + kaiSchedulerEnabled := commonController.DetectKaiSchedulerAvailability(mainCtx, mgr) + ctrlConfig.KaiScheduler.Enabled = kaiSchedulerEnabled + // Create etcd client cli, err := clientv3.New(clientv3.Config{ Endpoints: []string{etcdAddr}, diff --git a/deploy/cloud/operator/config/rbac/role.yaml b/deploy/cloud/operator/config/rbac/role.yaml index ca3770bad7..101f9f3939 100644 --- a/deploy/cloud/operator/config/rbac/role.yaml +++ b/deploy/cloud/operator/config/rbac/role.yaml @@ -185,6 +185,13 @@ rules: - get - patch - update +- apiGroups: + - scheduling.run.ai + resources: + - queues + verbs: + - get + - list - apiGroups: - scheduling.volcano.sh resources: diff --git a/deploy/cloud/operator/internal/consts/consts.go b/deploy/cloud/operator/internal/consts/consts.go index d76f0e4919..57622f5bf0 100644 --- a/deploy/cloud/operator/internal/consts/consts.go +++ b/deploy/cloud/operator/internal/consts/consts.go @@ -1,6 +1,10 @@ package consts -import "time" +import ( + "time" + + "k8s.io/apimachinery/pkg/runtime/schema" +) const ( HPACPUDefaultAverageUtilization = 80 @@ -55,6 +59,12 @@ const ( DefaultSharedMemoryMountPath = "/dev/shm" DefaultSharedMemorySize = "8Gi" + // Kai-scheduler related constants + KubeAnnotationKaiSchedulerQueue = "nvidia.com/kai-scheduler-queue" // User-provided annotation to specify queue name + KubeLabelKaiSchedulerQueue = "kai.scheduler/queue" // Label injected into pods for kai-scheduler + KaiSchedulerName = "kai-scheduler" // Scheduler name for kai-scheduler + DefaultKaiSchedulerQueue = "dynamo" // Default queue name when none specified + // Grove multinode role suffixes GroveRoleSuffixLeader = "ldr" GroveRoleSuffixWorker = "wkr" @@ -68,3 +78,25 @@ const ( MultinodeDeploymentTypeGrove MultinodeDeploymentType = "grove" MultinodeDeploymentTypeLWS MultinodeDeploymentType = "lws" ) + +// GroupVersionResources for external APIs +var ( + // Grove GroupVersionResources for scaling operations + PodCliqueGVR = schema.GroupVersionResource{ + Group: "grove.io", + Version: "v1alpha1", + Resource: "podcliques", + } + PodCliqueScalingGroupGVR = schema.GroupVersionResource{ + Group: "grove.io", + Version: "v1alpha1", + Resource: "podcliquescalinggroups", + } + + // KAI-Scheduler GroupVersionResource for queue validation + QueueGVR = schema.GroupVersionResource{ + Group: "scheduling.run.ai", + Version: "v2", + Resource: "queues", + } +) diff --git a/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go b/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go index afc2b6efb1..45545e57da 100644 --- a/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go +++ b/deploy/cloud/operator/internal/controller/dynamographdeployment_controller.go @@ -55,20 +55,6 @@ const ( PendingState State = "pending" ) -var ( - // Grove GroupVersionResources for scaling operations - podCliqueGVR = schema.GroupVersionResource{ - Group: "grove.io", - Version: "v1alpha1", - Resource: "podcliques", - } - podCliqueScalingGroupGVR = schema.GroupVersionResource{ - Group: "grove.io", - Version: "v1alpha1", - Resource: "podcliquescalinggroups", - } -) - type etcdStorage interface { DeleteKeys(ctx context.Context, prefix string) error } @@ -88,6 +74,7 @@ type DynamoGraphDeploymentReconciler struct { // +kubebuilder:rbac:groups=grove.io,resources=podgangsets,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=grove.io,resources=podcliques/scale,verbs=get;update;patch // +kubebuilder:rbac:groups=grove.io,resources=podcliquescalinggroups/scale,verbs=get;update;patch +// +kubebuilder:rbac:groups=scheduling.run.ai,resources=queues,verbs=get;list // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -201,9 +188,9 @@ func (r *DynamoGraphDeploymentReconciler) scaleGroveResource(ctx context.Context var gvr schema.GroupVersionResource switch resourceType { case "PodClique": - gvr = podCliqueGVR + gvr = consts.PodCliqueGVR case "PodCliqueScalingGroup": - gvr = podCliqueScalingGroupGVR + gvr = consts.PodCliqueScalingGroupGVR default: return fmt.Errorf("unsupported Grove resource type: %s", resourceType) } diff --git a/deploy/cloud/operator/internal/controller_common/predicate.go b/deploy/cloud/operator/internal/controller_common/predicate.go index ab65239396..14d10a8512 100644 --- a/deploy/cloud/operator/internal/controller_common/predicate.go +++ b/deploy/cloud/operator/internal/controller_common/predicate.go @@ -42,11 +42,17 @@ type LWSConfig struct { Enabled bool } +type KaiSchedulerConfig struct { + // Enabled is automatically determined by checking if Kai-scheduler CRDs are installed in the cluster + Enabled bool +} + type Config struct { // Enable resources filtering, only the resources belonging to the given namespace will be handled. RestrictedNamespace string Grove GroveConfig LWS LWSConfig + KaiScheduler KaiSchedulerConfig EtcdAddress string NatsAddress string IngressConfig IngressConfig @@ -75,6 +81,12 @@ func DetectLWSAvailability(ctx context.Context, mgr ctrl.Manager) bool { return detectAPIGroupAvailability(ctx, mgr, "leaderworkerset.x-k8s.io") } +// DetectKaiSchedulerAvailability checks if Kai-scheduler is available by checking if the scheduling.run.ai API group is registered +// This approach uses the discovery client which is simpler and more reliable +func DetectKaiSchedulerAvailability(ctx context.Context, mgr ctrl.Manager) bool { + return detectAPIGroupAvailability(ctx, mgr, "scheduling.run.ai") +} + // detectAPIGroupAvailability checks if a specific API group is registered in the cluster func detectAPIGroupAvailability(ctx context.Context, mgr ctrl.Manager, groupName string) bool { logger := log.FromContext(ctx) @@ -107,6 +119,7 @@ func detectAPIGroupAvailability(ctx context.Context, mgr ctrl.Manager, groupName logger.Info("API group not available", "group", groupName) return false } + func EphemeralDeploymentEventFilter(config Config) predicate.Predicate { return predicate.NewPredicateFuncs(func(o client.Object) bool { l := log.FromContext(context.Background()) diff --git a/deploy/cloud/operator/internal/dynamo/graph.go b/deploy/cloud/operator/internal/dynamo/graph.go index 962e56e5f2..c2148d403b 100644 --- a/deploy/cloud/operator/internal/dynamo/graph.go +++ b/deploy/cloud/operator/internal/dynamo/graph.go @@ -882,6 +882,17 @@ func GenerateGrovePodGangSet( if controllerConfig.Grove.TerminationDelay > 0 { gangSet.Spec.Template.TerminationDelay = &metav1.Duration{Duration: controllerConfig.Grove.TerminationDelay} } + + // Validate kai-scheduler queue once if kai-scheduler is enabled + var validatedQueueName string + if controllerConfig.Grove.Enabled && controllerConfig.KaiScheduler.Enabled { + var err error + validatedQueueName, err = DetermineKaiSchedulerQueue(ctx, dynamoDeployment.Annotations) + if err != nil { + return nil, fmt.Errorf("failed to determine kai-scheduler queue: %w", err) + } + } + dynamoNamespace, err := getDynamoNamespace(dynamoDeployment) if err != nil { return nil, fmt.Errorf("failed to get the graph dynamo namespace: %w", err) @@ -935,6 +946,10 @@ func GenerateGrovePodGangSet( return nil, fmt.Errorf("failed to generate annotations: %w", err) } clique.Annotations = annotations + + // Inject kai-scheduler settings if enabled + injectKaiSchedulerIfEnabled(clique, controllerConfig, validatedQueueName) + gangSet.Spec.Template.Cliques = append(gangSet.Spec.Template.Cliques, clique) cliqueNames = append(cliqueNames, strings.ToLower(r.Name)) } diff --git a/deploy/cloud/operator/internal/dynamo/grove.go b/deploy/cloud/operator/internal/dynamo/grove.go index 55fd68e780..910f16cb7d 100644 --- a/deploy/cloud/operator/internal/dynamo/grove.go +++ b/deploy/cloud/operator/internal/dynamo/grove.go @@ -14,6 +14,10 @@ import ( nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/v1alpha1" commonconsts "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts" + "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/controller_common" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" + ctrl "sigs.k8s.io/controller-runtime" ) type GroveMultinodeDeployer struct { @@ -130,3 +134,95 @@ func checkPCSGReady(ctx context.Context, client client.Client, resourceName, nam return true, "" } + +// resolveKaiSchedulerQueueName extracts the queue name from annotations or returns default +// This is the shared logic between DetermineKaiSchedulerQueue and ResolveKaiSchedulerQueue +func resolveKaiSchedulerQueueName(annotations map[string]string) string { + queueName := commonconsts.DefaultKaiSchedulerQueue + if annotations != nil { + if annotationQueue, exists := annotations[commonconsts.KubeAnnotationKaiSchedulerQueue]; exists && strings.TrimSpace(annotationQueue) != "" { + queueName = strings.TrimSpace(annotationQueue) + } + } + return queueName +} + +// ensureQueueExists validates that a Queue resource with the given name exists in the cluster +// Returns an error if the queue doesn't exist or if validation fails +func ensureQueueExists(ctx context.Context, dynamicClient dynamic.Interface, queueName string) error { + logger := log.FromContext(ctx) + + // Try to get the queue resource using the predefined GVR + _, err := dynamicClient.Resource(commonconsts.QueueGVR).Get(ctx, queueName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + logger.Error(err, "Queue not found", "queueName", queueName) + return fmt.Errorf("queue '%s' not found in cluster. Ensure the queue exists before using kai-scheduler", queueName) + } + logger.Error(err, "Failed to validate queue", "queueName", queueName) + return fmt.Errorf("failed to validate queue '%s': %w", queueName, err) + } + + logger.Info("Queue validation successful", "queueName", queueName) + return nil +} + +// DetermineKaiSchedulerQueue determines the queue name for kai-scheduler from deployment annotations or returns default +// Also validates that the queue exists in the cluster +func DetermineKaiSchedulerQueue(ctx context.Context, annotations map[string]string) (string, error) { + // Get the queue name from annotation or use default + queueName := resolveKaiSchedulerQueueName(annotations) + + // Create a dynamic client for CRD validation (Queue CRD might not be in the standard client scheme) + cfg, err := ctrl.GetConfig() + if err != nil { + return "", fmt.Errorf("failed to get kubernetes config for queue validation: %w", err) + } + + dynamicClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return "", fmt.Errorf("failed to create dynamic client for queue validation: %w", err) + } + + // Validate that the queue exists + if err := ensureQueueExists(ctx, dynamicClient, queueName); err != nil { + return "", fmt.Errorf("kai-scheduler queue validation failed: %w", err) + } + + return queueName, nil +} + +// ResolveKaiSchedulerQueue determines the queue name for kai-scheduler from deployment annotations or returns default +// Does NOT validate - use DetermineKaiSchedulerQueue for validation +func ResolveKaiSchedulerQueue(annotations map[string]string) string { + return resolveKaiSchedulerQueueName(annotations) +} + +// injectKaiSchedulerIfEnabled injects kai-scheduler settings into a clique if kai-scheduler is enabled and grove is enabled +func injectKaiSchedulerIfEnabled( + clique *grovev1alpha1.PodCliqueTemplateSpec, + controllerConfig controller_common.Config, + validatedQueueName string, +) { + // Only proceed if grove is enabled, kai-scheduler is enabled, and no manual schedulerName is set + if !controllerConfig.Grove.Enabled || !controllerConfig.KaiScheduler.Enabled { + return + } + + // Check if user has manually set schedulerName - if so, respect their choice + if clique.Spec.PodSpec.SchedulerName != "" && clique.Spec.PodSpec.SchedulerName != commonconsts.KaiSchedulerName { + return + } + + // Use the pre-validated queue name + queueName := validatedQueueName + + // Inject schedulerName + clique.Spec.PodSpec.SchedulerName = commonconsts.KaiSchedulerName + + // Inject queue label + if clique.Labels == nil { + clique.Labels = make(map[string]string) + } + clique.Labels[commonconsts.KubeLabelKaiSchedulerQueue] = queueName +} diff --git a/deploy/cloud/operator/internal/dynamo/grove_test.go b/deploy/cloud/operator/internal/dynamo/grove_test.go new file mode 100644 index 0000000000..acd469dc8b --- /dev/null +++ b/deploy/cloud/operator/internal/dynamo/grove_test.go @@ -0,0 +1,313 @@ +package dynamo + +import ( + "context" + "strings" + "testing" + + grovev1alpha1 "github.com/NVIDIA/grove/operator/api/core/v1alpha1" + commonconsts "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts" + "github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/controller_common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + dynamicfake "k8s.io/client-go/dynamic/fake" +) + +func TestResolveKaiSchedulerQueueName(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + expected string + }{ + { + name: "nil annotations", + annotations: nil, + expected: commonconsts.DefaultKaiSchedulerQueue, + }, + { + name: "empty annotations", + annotations: map[string]string{}, + expected: commonconsts.DefaultKaiSchedulerQueue, + }, + { + name: "no kai-scheduler annotation", + annotations: map[string]string{ + "other-annotation": "value", + }, + expected: commonconsts.DefaultKaiSchedulerQueue, + }, + { + name: "empty kai-scheduler annotation", + annotations: map[string]string{ + commonconsts.KubeAnnotationKaiSchedulerQueue: "", + }, + expected: commonconsts.DefaultKaiSchedulerQueue, + }, + { + name: "custom queue name", + annotations: map[string]string{ + commonconsts.KubeAnnotationKaiSchedulerQueue: "custom-queue", + }, + expected: "custom-queue", + }, + { + name: "whitespace is trimmed", + annotations: map[string]string{ + commonconsts.KubeAnnotationKaiSchedulerQueue: " custom-queue ", + }, + expected: "custom-queue", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := resolveKaiSchedulerQueueName(tt.annotations) + if result != tt.expected { + t.Errorf("resolveKaiSchedulerQueueName() = %v, expected %v", result, tt.expected) + } + }) + } +} + +func TestResolveKaiSchedulerQueue(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + expected string + }{ + { + name: "default queue", + annotations: nil, + expected: commonconsts.DefaultKaiSchedulerQueue, + }, + { + name: "custom queue", + annotations: map[string]string{ + commonconsts.KubeAnnotationKaiSchedulerQueue: "my-queue", + }, + expected: "my-queue", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ResolveKaiSchedulerQueue(tt.annotations) + if result != tt.expected { + t.Errorf("ResolveKaiSchedulerQueue() = %v, expected %v", result, tt.expected) + } + }) + } +} + +func TestInjectKaiSchedulerIfEnabled(t *testing.T) { + tests := []struct { + name string + controllerConfig controller_common.Config + validatedQueueName string + initialClique *grovev1alpha1.PodCliqueTemplateSpec + expectedScheduler string + expectedQueueLabel string + shouldInject bool + }{ + { + name: "grove disabled - no injection", + controllerConfig: controller_common.Config{ + Grove: controller_common.GroveConfig{Enabled: false}, + KaiScheduler: controller_common.KaiSchedulerConfig{Enabled: true}, + }, + validatedQueueName: "test-queue", + initialClique: &grovev1alpha1.PodCliqueTemplateSpec{ + Spec: grovev1alpha1.PodCliqueSpec{ + PodSpec: corev1.PodSpec{}, + }, + }, + shouldInject: false, + }, + { + name: "kai-scheduler disabled - no injection", + controllerConfig: controller_common.Config{ + Grove: controller_common.GroveConfig{Enabled: true}, + KaiScheduler: controller_common.KaiSchedulerConfig{Enabled: false}, + }, + validatedQueueName: "test-queue", + initialClique: &grovev1alpha1.PodCliqueTemplateSpec{ + Spec: grovev1alpha1.PodCliqueSpec{ + PodSpec: corev1.PodSpec{}, + }, + }, + shouldInject: false, + }, + { + name: "manual scheduler set - no injection", + controllerConfig: controller_common.Config{ + Grove: controller_common.GroveConfig{Enabled: true}, + KaiScheduler: controller_common.KaiSchedulerConfig{Enabled: true}, + }, + validatedQueueName: "test-queue", + initialClique: &grovev1alpha1.PodCliqueTemplateSpec{ + Spec: grovev1alpha1.PodCliqueSpec{ + PodSpec: corev1.PodSpec{ + SchedulerName: "manual-scheduler", + }, + }, + }, + shouldInject: false, + }, + { + name: "both enabled, no manual scheduler - inject", + controllerConfig: controller_common.Config{ + Grove: controller_common.GroveConfig{Enabled: true}, + KaiScheduler: controller_common.KaiSchedulerConfig{Enabled: true}, + }, + validatedQueueName: "test-queue", + initialClique: &grovev1alpha1.PodCliqueTemplateSpec{ + Spec: grovev1alpha1.PodCliqueSpec{ + PodSpec: corev1.PodSpec{}, + }, + }, + expectedScheduler: commonconsts.KaiSchedulerName, + expectedQueueLabel: "test-queue", + shouldInject: true, + }, + { + name: "inject with existing labels", + controllerConfig: controller_common.Config{ + Grove: controller_common.GroveConfig{Enabled: true}, + KaiScheduler: controller_common.KaiSchedulerConfig{Enabled: true}, + }, + validatedQueueName: "custom-queue", + initialClique: &grovev1alpha1.PodCliqueTemplateSpec{ + Labels: map[string]string{ + "existing-label": "existing-value", + }, + Spec: grovev1alpha1.PodCliqueSpec{ + PodSpec: corev1.PodSpec{}, + }, + }, + expectedScheduler: commonconsts.KaiSchedulerName, + expectedQueueLabel: "custom-queue", + shouldInject: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Make a deep copy to avoid modifying the test case + clique := tt.initialClique.DeepCopy() + + // Call the function + injectKaiSchedulerIfEnabled(clique, tt.controllerConfig, tt.validatedQueueName) + + if tt.shouldInject { + // Verify scheduler name is injected + if clique.Spec.PodSpec.SchedulerName != tt.expectedScheduler { + t.Errorf("expected schedulerName %v, got %v", tt.expectedScheduler, clique.Spec.PodSpec.SchedulerName) + } + + // Verify queue label is injected + if clique.Labels == nil { + t.Errorf("expected labels to be set, got nil") + } else { + queueLabel := clique.Labels[commonconsts.KubeLabelKaiSchedulerQueue] + if queueLabel != tt.expectedQueueLabel { + t.Errorf("expected queue label %v, got %v", tt.expectedQueueLabel, queueLabel) + } + } + + // Verify existing labels are preserved + if tt.initialClique.Labels != nil { + for key, value := range tt.initialClique.Labels { + if clique.Labels[key] != value { + t.Errorf("existing label %s=%s was not preserved, got %s", key, value, clique.Labels[key]) + } + } + } + } else { + // Verify no injection occurred + if clique.Spec.PodSpec.SchedulerName != tt.initialClique.Spec.PodSpec.SchedulerName { + t.Errorf("schedulerName should not have changed, expected %v, got %v", + tt.initialClique.Spec.PodSpec.SchedulerName, clique.Spec.PodSpec.SchedulerName) + } + + // Verify queue label was not added (unless it existed before) + if tt.initialClique.Labels == nil || tt.initialClique.Labels[commonconsts.KubeLabelKaiSchedulerQueue] == "" { + if clique.Labels != nil && clique.Labels[commonconsts.KubeLabelKaiSchedulerQueue] != "" { + t.Errorf("queue label should not have been added") + } + } + } + }) + } +} + +func TestEnsureQueueExists(t *testing.T) { + tests := []struct { + name string + queueName string + setupQueue bool + expectedError bool + errorContains string + }{ + { + name: "queue exists", + queueName: "existing-queue", + setupQueue: true, + expectedError: false, + }, + { + name: "queue does not exist", + queueName: "missing-queue", + setupQueue: false, + expectedError: true, + errorContains: "not found in cluster", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a fake dynamic client + dynamicScheme := runtime.NewScheme() + fakeDynamic := dynamicfake.NewSimpleDynamicClient(dynamicScheme) + + if tt.setupQueue { + // Create a fake queue resource + queueGVR := schema.GroupVersionResource{ + Group: "scheduling.run.ai", + Version: "v2", + Resource: "queues", + } + + queue := &unstructured.Unstructured{} + queue.SetAPIVersion("scheduling.run.ai/v2") + queue.SetKind("Queue") + queue.SetName(tt.queueName) + + _, err := fakeDynamic.Resource(queueGVR).Create(context.Background(), queue, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to create fake queue: %v", err) + } + } + + // This test is limited because we can't easily mock the dynamic client creation + // In a real test environment, you would set up a proper test cluster or use envtest + err := ensureQueueExists(context.Background(), fakeDynamic, tt.queueName) + + if tt.expectedError { + if err == nil { + t.Errorf("expected error but got none") + } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("expected error to contain %q, got %v", tt.errorContains, err) + } + } else { + // We expect an error here because we can't properly mock the dynamic client + // In a real test, this would work with proper test setup + if err == nil { + t.Logf("Queue validation passed (this is expected in unit tests)") + } + } + }) + } +} diff --git a/docs/guides/dynamo_deploy/multinode-deployment.md b/docs/guides/dynamo_deploy/multinode-deployment.md index 7fdbb9ab01..7654cfacd4 100644 --- a/docs/guides/dynamo_deploy/multinode-deployment.md +++ b/docs/guides/dynamo_deploy/multinode-deployment.md @@ -24,21 +24,22 @@ Dynamo supports multinode deployments through the `multinode` section in resourc For sophisticated multinode deployments, Dynamo integrates with advanced Kubernetes orchestration systems: -- **[Grove](https://github.com/NVIDIA/grove/blob/main/docs/installation.md)**: Network topology-aware gang scheduling and auto-scaling for AI workloads -- (optional) **[KAI-Scheduler](https://github.com/NVIDIA/KAI-Scheduler)**: Kubernetes native scheduler optimized for AI workloads at scale +- **[Grove](https://github.com/NVIDIA/grove)**: Network topology-aware gang scheduling and auto-scaling for AI workloads +- **[KAI-Scheduler](https://github.com/NVIDIA/KAI-Scheduler)**: Kubernetes native scheduler optimized for AI workloads at scale These systems provide enhanced scheduling capabilities including topology-aware placement, gang scheduling, and coordinated auto-scaling across multiple nodes. **Features Enabled with Grove:** -- Hierarchical gang scheduling with `PodGangSet` and `PodClique` +- Declarative composition of AI workloads - Multi-level horizontal auto-scaling - Custom startup ordering for components - Resource-aware rolling updates -[KAI-Scheduler](https://github.com/NVIDIA/KAI-Scheduler) is an optional enhancement that provides a Kubernetes native scheduler optimized for AI workloads at large scale. +[KAI-Scheduler](https://github.com/NVIDIA/KAI-Scheduler) is a Kubernetes native scheduler optimized for AI workloads at large scale. **Features Enabled with KAI-Scheduler:** +- Gang scheduling - Network topology-aware pod placement - AI workload-optimized scheduling algorithms - GPU resource awareness and allocation @@ -46,6 +47,14 @@ These systems provide enhanced scheduling capabilities including topology-aware - Integration with Grove for enhanced capabilities - Performance optimizations for large-scale deployments + +##### Prerequisites + +- [Grove](https://github.com/NVIDIA/grove/blob/main/docs/installation.md) installed on the cluster +- (Optional) [KAI-Scheduler](https://github.com/NVIDIA/KAI-Scheduler) installed on the cluster with default queue name `dynamo` created. You can use a different queue name by setting the `nvidia.com/kai-scheduler-queue` annotation on the DGD resource. + +KAI-Scheduler is optional but recommended for advanced scheduling capabilities. + #### Using LWS and Volcano LWS is a simple multinode deployment mechanism that allows you to deploy a workload across multiple nodes. @@ -58,16 +67,70 @@ Volcano is a Kubernetes native scheduler optimized for AI workloads at scale. It ## Core Concepts +### Orchestrator Selection Algorithm + +Dynamo automatically selects the best available orchestrator for multinode deployments using the following logic: + +#### When Both Grove and LWS are Available: +- **Grove is selected by default** (recommended for advanced AI workloads) +- **LWS is selected** if you explicitly set `nvidia.com/enable-grove: "false"` annotation on your DGD resource + +#### When Only One Orchestrator is Available: +- The installed orchestrator (Grove or LWS) is automatically selected + +#### Scheduler Integration: +- **With Grove**: Automatically integrates with [KAI-Scheduler](https://github.com/NVIDIA/KAI-Scheduler) when available, providing: + - Advanced queue management via `nvidia.com/kai-scheduler-queue` annotation + - AI-optimized scheduling policies + - Resource-aware workload placement +- **With LWS**: Uses Volcano scheduler for gang scheduling and resource coordination + +#### Configuration Examples: + +**Default (Grove with KAI-Scheduler):** +```yaml +apiVersion: nvidia.com/v1alpha1 +kind: DynamoGraphDeployment +metadata: + name: my-multinode-deployment + annotations: + nvidia.com/kai-scheduler-queue: "gpu-intensive" # Optional: defaults to "dynamo" +spec: + # ... your deployment spec +``` + +**Force LWS usage:** +```yaml +apiVersion: nvidia.com/v1alpha1 +kind: DynamoGraphDeployment +metadata: + name: my-multinode-deployment + annotations: + nvidia.com/enable-grove: "false" +spec: + # ... your deployment spec +``` + + ### The `multinode` Section The `multinode` section in a resource specification defines how many physical nodes the workload should span: ```yaml -multinode: - nodeCount: 2 -resources: - limits: - gpu: "2" # 2 GPUs per node +apiVersion: nvidia.com/v1alpha1 +kind: DynamoGraphDeployment +metadata: + name: my-multinode-deployment +spec: + # ... your deployment spec + services: + my-service: + ... + multinode: + nodeCount: 2 + resources: + limits: + gpu: "2" # 2 GPUs per node ``` ### GPU Distribution @@ -88,16 +151,28 @@ The tensor parallelism (`tp-size` or `--tp`) in your command/args must match the ```yaml # Example: 2 multinode.nodeCount × 4 GPUs = 8 total GPUs -multinode: - nodeCount: 2 -resources: - limits: - gpu: "4" - -# Command args must use tp-size=8 -args: - - "--tp-size" - - "8" # Must equal multinode.nodeCount × gpu +apiVersion: nvidia.com/v1alpha1 +kind: DynamoGraphDeployment +metadata: + name: my-multinode-deployment +spec: + # ... your deployment spec + services: + my-service: + ... + multinode: + nodeCount: 2 + resources: + limits: + gpu: "4" + extraPodSpec: + mainContainer: + ... + args: + # Command args must use tp-size=8 + - "--tp-size" + - "8" # Must equal multinode.nodeCount × gpu + ``` From 877164184bce91a303d030b9d7e09a27f65ba996 Mon Sep 17 00:00:00 2001 From: Dmitry Tokarev Date: Wed, 27 Aug 2025 23:05:08 -0400 Subject: [PATCH 53/82] chore: Update support_matrix.md (#2735) Signed-off-by: Dmitry Tokarev Signed-off-by: Jason Zhou --- docs/support_matrix.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/support_matrix.md b/docs/support_matrix.md index 749d9f511a..f25b48bcb9 100644 --- a/docs/support_matrix.md +++ b/docs/support_matrix.md @@ -69,6 +69,8 @@ If you are using a **GPU**, the following GPU models and architectures are suppo | **Base Container** | [25.03](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda-dl-base/tags) | | **TensorRT-LLM** | 1.0.0rc6 | | **NIXL** | 0.4.1 | +| **vLLM** | 0.10.1.1 | +| **SGLang** | 0.5.0rc2 | > [!Important] > Specific versions of TensorRT-LLM supported by Dynamo are subject to change. From 8eb06b1807f9a88927c8891b729de9ae96f937ad Mon Sep 17 00:00:00 2001 From: Kris Hung Date: Wed, 27 Aug 2025 20:41:56 -0700 Subject: [PATCH 54/82] feat: Add vLLM multimodal video support (#2738) Signed-off-by: krishung5 Signed-off-by: Jason Zhou --- examples/multimodal/README.md | 176 ++++++++ .../multimodal/components/encode_worker.py | 9 +- examples/multimodal/components/processor.py | 30 +- .../components/video_encode_worker.py | 322 ++++++++++++++ examples/multimodal/components/worker.py | 39 +- examples/multimodal/launch/video_agg.sh | 23 + examples/multimodal/launch/video_disagg.sh | 25 ++ examples/multimodal/utils/http_client.py | 47 ++ examples/multimodal/utils/image_loader.py | 16 +- examples/multimodal/utils/model.py | 79 ++-- examples/multimodal/utils/protocol.py | 33 +- examples/multimodal/utils/video_utils.py | 414 ++++++++++++++++++ lib/async-openai/src/types/chat.rs | 24 + lib/async-openai/src/types/message.rs | 19 +- lib/async-openai/src/types/responses.rs | 24 +- tests/serve/test_vllm.py | 42 +- 16 files changed, 1233 insertions(+), 89 deletions(-) create mode 100644 examples/multimodal/components/video_encode_worker.py create mode 100755 examples/multimodal/launch/video_agg.sh create mode 100755 examples/multimodal/launch/video_disagg.sh create mode 100644 examples/multimodal/utils/http_client.py create mode 100644 examples/multimodal/utils/video_utils.py diff --git a/examples/multimodal/README.md b/examples/multimodal/README.md index fbadd97001..f2c0f96d2c 100644 --- a/examples/multimodal/README.md +++ b/examples/multimodal/README.md @@ -326,3 +326,179 @@ You should see a response similar to this: ```json {"id": "6cc99123ad6948d685b8695428238d4b", "object": "chat.completion", "created": 1752708043, "model": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", "choices": [{"index": 0, "message": {"role": "assistant", "content": "The image depicts a street scene with a trolley bus as the central focus. The trolley bus is positioned on the left side of the road, facing the camera, and features a white and yellow color scheme. A prominent sign on the front of the bus reads \"OUT OF SERVICE\" in orange letters.\n\n**Key Elements:**\n\n* **Trolley Bus:** The bus is the main subject of the image, showcasing its distinctive design and color.\n* **Sign:** The \"OUT OF SERVICE\" sign is clearly visible on the front of the bus, indicating its current status.\n* **Street Scene:** The surrounding environment includes trees, buildings, and power lines, creating a sense of context and atmosphere.\n* **Lighting:** The image is characterized by a misty or foggy quality, with soft lighting that adds to the overall mood.\n\n**Overall Impression:**\n\nThe image presents a serene and somewhat melancholic scene, with the out-of-service trolley bus serving as a focal point. The misty atmosphere and soft lighting contribute to a contemplative ambiance, inviting the viewer to reflect on the situation."}, "finish_reason": "stop"}]} ``` + +## Multimodal Aggregated Video Serving + +This example demonstrates deploying an aggregated multimodal model that can process video inputs. + +### Components + +- workers: For video serving, we use the [VideoEncodeWorker](components/video_encode_worker.py) for decoding video into frames, and send the frames to [VllmPDWorker](components/worker.py) for prefilling and decoding. +- processor: Tokenizes the prompt and passes it to the VideoEncodeWorker. +- frontend: HTTP endpoint to handle incoming requests. + +### Graph + +In this graph, we have two workers, [VideoEncodeWorker](components/video_encode_worker.py) and [VllmPDWorker](components/worker.py). +The VideoEncodeWorker is responsible for decoding the video into a series of frames. Unlike the image pipeline which generates embeddings, +this pipeline passes the raw frames directly to the VllmPDWorker via a combination of NATS and RDMA. +Its VllmPDWorker then prefills and decodes the prompt, just like the [LLM aggregated serving](/components/backends/vllm/README.md) example. +By separating the video processing from the prefill and decode stages, we can have a more flexible deployment and scale the +VideoEncodeWorker independently from the prefill and decode workers if needed. + +This figure shows the flow of the graph: +```mermaid +flowchart LR + HTTP --> processor + processor --> HTTP + processor --video_url--> video_encode_worker + video_encode_worker --> processor + video_encode_worker --frames--> pd_worker + pd_worker --> video_encode_worker +``` + +```bash +cd $DYNAMO_HOME/examples/multimodal +bash launch/video_agg.sh +``` + +### Client + +In another terminal: +```bash +curl http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "llava-hf/LLaVA-NeXT-Video-7B-hf", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe the video in detail" + }, + { + "type": "video_url", + "video_url": { + "url": "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + } + } + ] + } + ], + "max_tokens": 300, + "stream": false + }' | jq +``` + +You should see a response describing the video's content similar to +```json +{ + "id": "7587e7d152014bae8e5c4e25f9fda0ed", + "choices": [ + { + "index": 0, + "message": { + "content": " The video takes us away to a lively world of wildlife and natural beauty, featuring a white rabbit in a vibrant forest setting. At the beginning of the clip, the white rabbit is seen standing on a rock, facing towards the right side of the frame, with bushes and trees in the backdrop. The rabbit appears to be alert, given its ears are up and its ears perked in the air. As the clip progresses, the movement of the rabbit brings it around a tree, where its legs are partially hidden by the dense vegetation. It then sits down and grooms its fur, a behavior that suggests it is comfortable in its surroundings. \n\nThe scene then switches to a close-up shot of the rabbit, giving us a better view of its features and expressions. In this camera angle, the rabbit appears more dynamic and alert, with its breathing more visible, signaling its health and well-being. The camera pans out, and we see the rabbit heading towards the left side of the screen, possibly curious or hunting for food, with its ears perked up again. The lush greenery of the forest unfolds in the background, adding to the feeling of a wild and thriving environment.\n\n\nThe rabbit, upturned slightly while walking, finds a pile of dirt and rocks and sits there, fully clothed, perhaps taking a break from its exploration. There's a mention of a blue bird that appears to perch atop a log, adding a touch of whimsy to the scene. Lastly, the rabbit is observed relaxing on the rocks, resting comfortably, and looking off to the right side—a moment of tranquility in a bustling ecosystem. Throughout the clip, the rabbit's outfit remains the same, allowing for a clear focus on its behavior and characteristics while fitting in its habitat.", + "role": "assistant", + "reasoning_content": null + }, + "finish_reason": "stop" + } + ], + "created": 1756251832, + "model": "llava-hf/LLaVA-NeXT-Video-7B-hf", + "object": "chat.completion", + "usage": null +} +``` + +## Multimodal Disaggregated Video Serving + +This example demonstrates deploying a disaggregated multimodal model that can process video inputs. + +### Components + +- workers: For disaggregated video serving, we have three workers, [VideoEncodeWorker](components/video_encode_worker.py) for decoding video into frames, +[VllmDecodeWorker](components/worker.py) for decoding, and [VllmPDWorker](components/worker.py) for prefilling. +- processor: Tokenizes the prompt and passes it to the VideoEncodeWorker. +- frontend: HTTP endpoint to handle incoming requests. + +### Graph + +In this graph, we have three workers, [VideoEncodeWorker](components/video_encode_worker.py), [VllmDecodeWorker](components/worker.py), and [VllmPDWorker](components/worker.py). +For the LLaVA-NeXT-Video-7B model, frames are only required during the prefill stage. As such, the VideoEncodeWorker is connected directly to the prefill worker. +The VideoEncodeWorker is responsible for decoding the video into a series of frames and passing them to the prefill worker via RDMA. +The prefill worker performs the prefilling step and forwards the KV cache to the decode worker for decoding. +For more details on the roles of the prefill and decode workers, refer to the [LLM disaggregated serving](/components/backends/vllm/README.md) example. + +This figure shows the flow of the graph: +```mermaid +flowchart LR + HTTP --> processor + processor --> HTTP + processor --video_url--> video_encode_worker + video_encode_worker --> processor + video_encode_worker --frames--> prefill_worker + prefill_worker --> video_encode_worker + prefill_worker --> decode_worker + decode_worker --> prefill_worker +``` + +```bash +cd $DYNAMO_HOME/examples/multimodal +bash launch/video_disagg.sh +``` + +### Client + +In another terminal: +```bash +curl http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "llava-hf/LLaVA-NeXT-Video-7B-hf", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe the video in detail" + }, + { + "type": "video_url", + "video_url": { + "url": "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + } + } + ] + } + ], + "max_tokens": 300, + "stream": false + }' | jq +``` + +You should see a response describing the video's content similar to +```json +{ + "id": "7587e7d152014bae8e5c4e25f9fda0ed", + "choices": [ + { + "index": 0, + "message": { + "content": " The video takes us away to a lively world of wildlife and natural beauty, featuring a white rabbit in a vibrant forest setting. At the beginning of the clip, the white rabbit is seen standing on a rock, facing towards the right side of the frame, with bushes and trees in the backdrop. The rabbit appears to be alert, given its ears are up and its ears perked in the air. As the clip progresses, the movement of the rabbit brings it around a tree, where its legs are partially hidden by the dense vegetation. It then sits down and grooms its fur, a behavior that suggests it is comfortable in its surroundings. \n\nThe scene then switches to a close-up shot of the rabbit, giving us a better view of its features and expressions. In this camera angle, the rabbit appears more dynamic and alert, with its breathing more visible, signaling its health and well-being. The camera pans out, and we see the rabbit heading towards the left side of the screen, possibly curious or hunting for food, with its ears perked up again. The lush greenery of the forest unfolds in the background, adding to the feeling of a wild and thriving environment.\n\n\nThe rabbit, upturned slightly while walking, finds a pile of dirt and rocks and sits there, fully clothed, perhaps taking a break from its exploration. There's a mention of a blue bird that appears to perch atop a log, adding a touch of whimsy to the scene. Lastly, the rabbit is observed relaxing on the rocks, resting comfortably, and looking off to the right side—a moment of tranquility in a bustling ecosystem. Throughout the clip, the rabbit's outfit remains the same, allowing for a clear focus on its behavior and characteristics while fitting in its habitat.", + "role": "assistant", + "reasoning_content": null + }, + "finish_reason": "stop" + } + ], + "created": 1756251832, + "model": "llava-hf/LLaVA-NeXT-Video-7B-hf", + "object": "chat.completion", + "usage": null +} +``` diff --git a/examples/multimodal/components/encode_worker.py b/examples/multimodal/components/encode_worker.py index 09c222199a..25462d9f69 100644 --- a/examples/multimodal/components/encode_worker.py +++ b/examples/multimodal/components/encode_worker.py @@ -106,7 +106,12 @@ async def generate( # 8. Yield the encode response. try: - image = await self.image_loader.load_image(request.image_url) + if not request.multimodal_input.image_url: + raise ValueError("image_url is required for the encode worker.") + + image = await self.image_loader.load_image( + request.multimodal_input.image_url + ) logger.debug(f"Processing image for request: {{ id: {request_id} }}") image_embeds = self.image_processor(images=image, return_tensors="pt") @@ -135,7 +140,7 @@ async def generate( with self._connector.create_readable(descriptor) as readable: request.serialized_request = readable.metadata() # Clear the image URL as hint that the image is passed as embeddings. - request.image_url = None + request.multimodal_input.image_url = None logger.debug(f"Request: {request.model_dump_json()}") diff --git a/examples/multimodal/components/processor.py b/examples/multimodal/components/processor.py index d155a033bb..05d5ff8ef4 100644 --- a/examples/multimodal/components/processor.py +++ b/examples/multimodal/components/processor.py @@ -40,13 +40,16 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) from utils.args import Config, base_parse_args, parse_endpoint from utils.chat_processor import ChatProcessor, CompletionsProcessor, ProcessMixIn -from utils.protocol import MultiModalRequest, MyRequestOutput, vLLMMultimodalRequest +from utils.protocol import ( + MultiModalInput, + MultiModalRequest, + MyRequestOutput, + vLLMMultimodalRequest, +) configure_dynamo_logging() logger = logging.getLogger(__name__) -prompt_template = "USER: \n ASSISTANT:" - class RequestType(Enum): CHAT = "chat" @@ -134,7 +137,7 @@ def _create_tokenizer(self, engine_args: AsyncEngineArgs) -> AnyTokenizer: async def _generate( self, raw_request: Union[CompletionRequest, ChatCompletionRequest], - image: str, + multimodal_input: MultiModalInput, request_type: RequestType, ): request_id = str(uuid.uuid4().hex) @@ -151,7 +154,7 @@ async def _generate( engine_prompt=engine_prompt, sampling_params=sampling_params, request_id=request_id, - image_url=image, + multimodal_input=multimodal_input, ) # model_dump_json() serializes the request to JSON string @@ -233,16 +236,23 @@ async def generate(self, raw_request: MultiModalRequest): temperature=raw_request.temperature, request_id=str(uuid.uuid4()), ) - image_url = None + multimodal_input = MultiModalInput() for message in raw_request.messages: for item in message.content: if item.type == "image_url": - image_url = item.image_url.url - if image_url is None: - raise ValueError("Image URL is required") + multimodal_input.image_url = item.image_url.url + elif item.type == "video_url": + if multimodal_input.image_url is not None: + raise ValueError("Cannot provide both image and video URLs") + multimodal_input.video_url = item.video_url.url + + if multimodal_input.image_url is None and multimodal_input.video_url is None: + raise ValueError("Either image URL or video URL is required") - async for response in self._generate(chat_request, image_url, RequestType.CHAT): + async for response in self._generate( + chat_request, multimodal_input, RequestType.CHAT + ): logger.debug( f"Generated response type {type(response)}, content: {response}" ) diff --git a/examples/multimodal/components/video_encode_worker.py b/examples/multimodal/components/video_encode_worker.py new file mode 100644 index 0000000000..1400cb4b7b --- /dev/null +++ b/examples/multimodal/components/video_encode_worker.py @@ -0,0 +1,322 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +import argparse +import asyncio +import logging +import os +import signal +import sys +from io import BytesIO +from queue import Queue +from typing import AsyncIterator, Optional, Tuple + +import av +import numpy as np +import torch +import uvloop +from vllm.engine.arg_utils import AsyncEngineArgs +from vllm.utils import FlexibleArgumentParser + +import dynamo.nixl_connect as connect +from dynamo.runtime import Client, DistributedRuntime, dynamo_worker +from dynamo.runtime.logging import configure_dynamo_logging + +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from utils.args import Config, base_parse_args, parse_endpoint +from utils.protocol import MyRequestOutput, vLLMMultimodalRequest +from utils.video_utils import ( + calculate_frame_sampling_indices, + get_video_metadata, + load_video_content, + open_video_container, + prepare_tensor_for_rdma, + read_video_pyav, + resize_video_frames, +) + +configure_dynamo_logging() +logger = logging.getLogger(__name__) + +try: + import cupy as array_module + + if not array_module.cuda.is_available(): + raise ImportError("CUDA is not available.") + DEVICE = "cuda" + logger.info("Using cupy for array operations (GPU mode).") +except ImportError as e: + logger.warning(f"Failed to import cupy, falling back to numpy: {e}.") + import numpy as array_module + + DEVICE = "cpu" + +CACHE_SIZE_MAXIMUM = 8 + + +class VllmEncodeWorker: + def __init__( + self, + args: argparse.Namespace, + engine_args: AsyncEngineArgs, + pd_worker_client: Client, + ) -> None: + self.pd_worker_client = pd_worker_client + self.engine_args = engine_args + self.model = self.engine_args.model + self.min_workers = 1 + + # Video processing parameters + self.num_frames_to_sample = args.num_frames_to_sample + self.frame_height = 336 + self.frame_width = 336 + self.frame_channels = 3 + self._video_content_cache: dict[str, BytesIO] = {} + self._cache_queue: Queue[str] = Queue(maxsize=CACHE_SIZE_MAXIMUM) + + self._http_timeout = 60.0 + + def cleanup(self): + pass + + async def generate( + self, request: vLLMMultimodalRequest + ) -> AsyncIterator[MyRequestOutput]: + logger.debug(f"Got raw request: {request}") + if not isinstance(request, vLLMMultimodalRequest): + if isinstance(request, str): + request = vLLMMultimodalRequest.model_validate_json(request) + else: + request = vLLMMultimodalRequest.model_validate(request) + logger.debug(f"Received encode request: {{ id: {request.request_id} }}.") + + request_id = request.request_id + video_url = request.multimodal_input.video_url + + if video_url is None: + raise ValueError("Video URL is required.") + + container: Optional[av.container.InputContainer] = None + + try: + video_content_stream = await load_video_content( + video_url, + self._video_content_cache, + self._cache_queue, + self._http_timeout, + ) + + # Open video container using utility function + container = await open_video_container(video_content_stream, video_url) + + if not container or not container.streams.video: + logger.error(f"No video stream found in {video_url}.") + raise ValueError(f"No video stream in {video_url}.") + + # Get video metadata using utility function + total_frames, duration_sec = get_video_metadata(container) + + # Calculate frame sampling indices using utility function + indices = calculate_frame_sampling_indices( + total_frames, self.num_frames_to_sample, duration_sec, video_url + ) + + if not container: + raise ValueError(f"Container is None for {video_url}") + + # Decode video frames + clip_np: np.ndarray = await read_video_pyav(container, indices) + + if clip_np.size == 0: + raise ValueError( + f"Failed to extract any video frames from {video_url} for indices {indices.tolist()}. Clip is empty." + ) + + logger.debug( + f"Successfully extracted {len(clip_np) if clip_np.ndim > 1 and clip_np.shape[0] > 0 else 0} frames for {video_url} with original shape {clip_np.shape}." + ) + + # Convert the NumPy array from the video decoder into a PyTorch tensor. + # This is a required step to use PyTorch functions for GPU-accelerated image processing. + frames_tensor_orig_res = torch.from_numpy(clip_np) # Shape: (T, H, W, C) + + # Resize frames using utility function + resized_frames_tensor_hwc = resize_video_frames( + frames_tensor_orig_res, self.frame_height, self.frame_width + ) + + # Prepare tensor for RDMA using utility function + tensor_for_descriptor = prepare_tensor_for_rdma( + resized_frames_tensor_hwc, request_id + ) + + request.embeddings_shape = tuple(tensor_for_descriptor.shape) + descriptor = connect.Descriptor(tensor_for_descriptor) + + with self._connector.create_readable(descriptor) as readable: + request.serialized_request = readable.metadata() + # Clear the image URL as hint that the image is passed as embeddings. + request.multimodal_input.video_url = None + + logger.debug(f"Request: {request.model_dump_json()}") + + # Get the response generator + response_generator = await self.pd_worker_client.round_robin( + request.model_dump_json() + ) + await readable.wait_for_completion() + + async for response in response_generator: + output = MyRequestOutput.model_validate_json(response.data()) + yield MyRequestOutput( + request_id=output.request_id, + prompt=output.prompt, + prompt_token_ids=output.prompt_token_ids, + prompt_logprobs=output.prompt_logprobs, + outputs=output.outputs, + finished=output.finished, + ).model_dump_json() + except ( + FileNotFoundError, + av.FFmpegError, + ValueError, + ) as e: + logger.error( + f"Error processing request {request_id} ({video_url[:100]}...): {type(e).__name__} - {e}" + ) + raise # Re-raise to be handled by the service framework + except Exception as e: + logger.exception( + f"Unexpected error processing request {request_id} ({video_url[:100]}...): {e}" + ) + raise + finally: + if container: + await asyncio.to_thread(container.close) + + async def async_init(self, runtime: DistributedRuntime): + logger.info("Startup started.") + # Create and initialize a dynamo connector for this worker. + # We'll needs this to move data between this worker and remote workers efficiently. + self._connector = connect.Connector() + await self._connector.initialize() + + logger.info("Startup completed.") + + @classmethod + def parse_args(cls) -> Tuple[argparse.Namespace, Config]: + DEFAULT_ENDPOINT = "dyn://dynamo.encoder.generate" + DEFAULT_DOWNSTREAM_ENDPOINT = "dyn://dynamo.llm.generate" + + parser = FlexibleArgumentParser( + description="vLLM based encoder for Dynamo LLM." + ) + parser.add_argument( + "--endpoint", + type=str, + default=DEFAULT_ENDPOINT, + help=f"Dynamo endpoint string in 'dyn://namespace.component.endpoint' format. Default: '{DEFAULT_ENDPOINT}'", + ) + parser.add_argument( + "--downstream-endpoint", + type=str, + default=DEFAULT_DOWNSTREAM_ENDPOINT, + help=f"The endpoint string of the downstream LLM in 'dyn://namespace.component.endpoint' format. Default: '{DEFAULT_DOWNSTREAM_ENDPOINT}'", + ) + parser.add_argument( + "--num-frames-to-sample", + type=int, + default=8, + help="Number of frames to sample from the video. Default: 8", + ) + + args, config = base_parse_args(parser) + + return args, config + + +async def graceful_shutdown(runtime): + """ + By calling `runtime.shutdown()`, the endpoints will immediately be unavailable. + However, in-flight requests will still be processed until they are finished. + After all in-flight requests are finished, the `serve_endpoint` functions will return + and the engine will be shutdown by Python's garbage collector. + """ + logging.info("Received shutdown signal, shutting down DistributedRuntime") + runtime.shutdown() + logging.info("DistributedRuntime shutdown complete") + + +@dynamo_worker(static=False) +async def worker(runtime: DistributedRuntime): + # Runtime setup + # Set up signal handler for graceful shutdown + loop = asyncio.get_running_loop() + + def signal_handler(): + asyncio.create_task(graceful_shutdown(runtime)) + + for sig in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(sig, signal_handler) + + logging.info("Signal handlers set up for graceful shutdown") + + # worker setup + args, config = VllmEncodeWorker.parse_args() + await init(runtime, args, config) + + +async def init(runtime: DistributedRuntime, args: argparse.Namespace, config: Config): + """ + Instantiate and serve + """ + + component = runtime.namespace(config.namespace).component(config.component) + await component.create_service() + + generate_endpoint = component.endpoint(config.endpoint) + + parsed_namespace, parsed_component_name, parsed_endpoint_name = parse_endpoint( + args.downstream_endpoint + ) + pd_worker_client = ( + await runtime.namespace(parsed_namespace) + .component(parsed_component_name) + .endpoint(parsed_endpoint_name) + .client() + ) + + handler = VllmEncodeWorker(args, config.engine_args, pd_worker_client) + await handler.async_init(runtime) + + logger.info("Waiting for PD Worker Instances ...") + await pd_worker_client.wait_for_instances() + + logger.info(f"Starting to serve the {args.endpoint} endpoint...") + + try: + await asyncio.gather( + generate_endpoint.serve_endpoint(handler.generate), + ) + except Exception as e: + logger.error(f"Failed to serve endpoints: {e}") + raise + finally: + handler.cleanup() + + +if __name__ == "__main__": + uvloop.install() + asyncio.run(worker()) diff --git a/examples/multimodal/components/worker.py b/examples/multimodal/components/worker.py index a549088158..0f855e2044 100644 --- a/examples/multimodal/components/worker.py +++ b/examples/multimodal/components/worker.py @@ -245,8 +245,13 @@ async def async_init(self, runtime: DistributedRuntime): .client() ) - self.EMBEDDINGS_DTYPE = torch.float16 + if "video" in self.engine_args.model.lower(): + self.EMBEDDINGS_DTYPE = torch.uint8 + else: + self.EMBEDDINGS_DTYPE = torch.float16 + self.EMBEDDINGS_DEVICE = "cpu" + # Create and initialize a dynamo connector for this worker. # We'll needs this to move data between this worker and remote workers efficiently. parsed_namespace, _, _ = parse_endpoint(self.endpoint) @@ -277,7 +282,10 @@ async def generate(self, request: vLLMMultimodalRequest): ) descriptor = connect.Descriptor(embeddings) - if request.image_url is None: + if ( + request.multimodal_input.image_url is None + and request.multimodal_input.video_url is None + ): if descriptor is None: raise RuntimeError( "Descriptor is None in PD worker - cannot process embeddings" @@ -287,20 +295,31 @@ async def generate(self, request: vLLMMultimodalRequest): request.serialized_request, descriptor ) await read_op.wait_for_completion() - multi_modal_data = construct_mm_data( - self.engine_args.model, - embeddings, - self.EMBEDDINGS_DTYPE, - request.image_grid_thw, - ) + if "video" in self.engine_args.model.lower(): + video_numpy = embeddings.numpy() + multi_modal_data = construct_mm_data( + self.engine_args.model, + self.EMBEDDINGS_DTYPE, + video_numpy=video_numpy, + ) + else: + multi_modal_data = construct_mm_data( + self.engine_args.model, + self.EMBEDDINGS_DTYPE, + image_embeds=embeddings, + image_grid_thw=request.image_grid_thw, + ) else: # Use PIL image instead of image embeddings multi_modal_data = { - "image": await self.image_loader.load_image(request.image_url) + "image": await self.image_loader.load_image( + request.multimodal_input.image_url + ) } # Remove the image features from the request as they are not required - request.image_url = None + request.multimodal_input.image_url = None + request.multimodal_input.video_url = None request.serialized_request = None pd_request = copy.deepcopy(request) diff --git a/examples/multimodal/launch/video_agg.sh b/examples/multimodal/launch/video_agg.sh new file mode 100755 index 0000000000..299227e040 --- /dev/null +++ b/examples/multimodal/launch/video_agg.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +set -e +trap 'echo Cleaning up...; kill 0' EXIT + +# Default values +MODEL_NAME="llava-hf/LLaVA-NeXT-Video-7B-hf" +PROMPT_TEMPLATE="USER:

M`A1zb4r(L;jOGe%jK7yPyz9@?vr^Vah6 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 *R0xULByC|~ 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 literal 740824 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