Files
ray-project--ray/python/ray/llm/_internal/serve/utils/registry.py
T
2026-07-13 13:17:40 +08:00

329 lines
11 KiB
Python

"""Generic registry for LLM serving components using Ray's internal KV store.
This module provides a reusable registry mechanism that enables components to be
registered in the driver process and accessed across all Ray processes in the cluster,
including Ray Serve child processes.
Similar to RLlib/Tune's registry but with a fixed global prefix for cross-job access.
"""
import importlib
from typing import Any, Callable
import ray._private.worker as worker
import ray.cloudpickle as pickle
from ray.experimental.internal_kv import (
_internal_kv_del,
_internal_kv_exists,
_internal_kv_get,
_internal_kv_initialized,
_internal_kv_put,
)
from ray.llm._internal.serve.observability.logging import get_logger
logger = get_logger(__name__)
# Fixed prefix for cross-job accessibility (Serve deployments run in different jobs)
_SERVE_REGISTRY_PREFIX = "serve_global"
def _make_key(category: str, name: str) -> bytes:
"""Generate a binary key for the KV store.
Args:
category: The component category (e.g., "kv_connector_backend")
name: The component name
Returns:
The key to use for storing the value
"""
return (
b"LLMServeRegistry:"
+ _SERVE_REGISTRY_PREFIX.encode("ascii")
+ b":"
+ category.encode("ascii")
+ b"/"
+ name.encode("ascii")
)
def _create_loader(value: Any) -> Callable[[], Any]:
"""Create a loader callable for a value.
Handles both direct objects/classes and string paths for lazy loading.
Args:
value: Either:
- A class, object, or callable (returns lambda: value)
- A string in format "module_path:class_name" (creates import loader)
Returns:
A callable that returns the value when called
Raises:
ValueError: If value is a string but doesn't have the correct format
"""
if isinstance(value, str):
if ":" not in value:
raise ValueError(
f"Invalid format for string value: '{value}'. "
f"Expected format: 'module_path:class_name' or a class/object."
)
module_path, class_name = value.rsplit(":", 1)
# Create a loader callable that imports on demand
def loader():
module = importlib.import_module(module_path)
return getattr(module, class_name)
return loader
else:
# For direct objects/classes, create a simple loader
return lambda: value
class ComponentRegistry:
"""Generic registry for LLM serving components using Ray's internal KV store.
This registry enables components to be registered in the driver process and
accessed across all Ray processes in the cluster, including Ray Serve child processes.
Similar to RLlib/Tune's registry but with a fixed global prefix for cross-job access.
**Usage Pattern:**
This registry is designed for a "register once, read many" pattern:
- Components are typically registered in the driver process before deployment
- Ray Serve replicas read from the KV store during initialization
- Once a component is resolved and cached in a process, subsequent `get()` calls return the cached value without checking the KV store for updates
Example:
# Create a registry for a component category
registry = ComponentRegistry("my_component")
# Register a component
registry.register("my_component", MyComponentClass)
# Get a registered component
component = registry.get("my_component")
# Check if registered
if registry.contains("my_component"):
...
"""
def __init__(self, category: str):
"""Initialize a registry for a specific component category.
Args:
category: The category name (e.g., "kv_connector_backend")
"""
self.category = category
self._loader_cache: dict[str, Callable[[], Any]] = {}
self._resolved_cache: dict[str, Any] = {}
self._pending: dict[str, bytes] = {}
def register(self, name: str, value: Any) -> None:
"""Register a component.
Args:
name: The name to register under
value: The component to register. Can be:
- A class, object, or callable (serialized directly)
- A string in format "module_path:class_name" (lazy-loaded via import)
Raises:
ValueError: If the component is already registered. Use unregister() first if you need to change the registration.
Examples:
# Register a class directly
registry.register("MyClass", MyClass)
# Register via module path (lazy loading)
registry.register("MyClass", "my.module:MyClass")
"""
# Prevent double registration to avoid cache inconsistencies
if self.contains(name):
raise ValueError(
f"{self.category} '{name}' is already registered. "
f"Use unregister() first if you need to change the registration."
)
# Create a loader callable (handles both direct values and string paths)
loader = _create_loader(value)
# Serialize the loader callable
serialized = pickle.dumps(loader)
# Store loader in cache
self._loader_cache[name] = loader
# Store in KV store if Ray is initialized, otherwise queue for later
if _internal_kv_initialized():
try:
key = _make_key(self.category, name)
_internal_kv_put(key, serialized, overwrite=True)
logger.debug(f"Registered {self.category} '{name}' in KV store")
except Exception as e:
logger.warning(
f"Failed to register {self.category} '{name}' in KV store: {e}",
exc_info=True,
)
self._pending[name] = serialized
else:
self._pending[name] = serialized
def get(self, name: str) -> Any:
"""Get a registered component.
Args:
name: The name of the component
Returns:
The registered component. If registered with a string path,
returns the imported class/object. If registered directly,
returns the original value.
Raises:
ValueError: If the component is not registered
"""
# Check resolved cache first.
if name in self._resolved_cache:
return self._resolved_cache[name]
loader = self._loader_cache.get(name)
# If not in local loader cache, try fetching from KV store.
if loader is None and _internal_kv_initialized():
try:
key = _make_key(self.category, name)
serialized = _internal_kv_get(key)
if serialized is not None:
loader = pickle.loads(serialized)
# Cache the loader for future gets.
self._loader_cache[name] = loader
logger.debug(f"Loaded {self.category} '{name}' from KV store")
except Exception as e:
logger.warning(
f"Failed to load {self.category} '{name}' from KV store: {e}",
exc_info=True,
)
if loader is not None:
value = loader()
self._resolved_cache[name] = value
return value
# Not found
raise ValueError(
f"{self.category} '{name}' not found. "
f"Registered: {list(self._loader_cache.keys())}"
)
def contains(self, name: str) -> bool:
"""Check if a component is registered.
Args:
name: The name to check
Returns:
True if registered, False otherwise
"""
if name in self._loader_cache:
return True
if _internal_kv_initialized():
try:
key = _make_key(self.category, name)
return _internal_kv_exists(key)
except Exception as e:
logger.warning(
f"Failed to check if {self.category} '{name}' exists in KV store: {e}",
exc_info=True,
)
return False
return False
def unregister(self, name: str) -> None:
"""Unregister a component.
Removes the component from local cache, pending registrations, and KV store.
Args:
name: The name of the component to unregister
"""
# Remove from local caches
if name in self._loader_cache:
del self._loader_cache[name]
if name in self._resolved_cache:
del self._resolved_cache[name]
# Remove from pending if present
if name in self._pending:
del self._pending[name]
# Remove from KV store if Ray is initialized
if _internal_kv_initialized():
try:
key = _make_key(self.category, name)
_internal_kv_del(key)
logger.debug(f"Unregistered {self.category} '{name}' from KV store")
except Exception as e:
logger.warning(
f"Failed to unregister {self.category} '{name}' from KV store: {e}",
exc_info=True,
)
def flush_pending(self) -> None:
"""Flush pending registrations to KV store.
This is called automatically when Ray initializes via _post_init_hooks.
"""
if not _internal_kv_initialized() or not self._pending:
return
for name, serialized in self._pending.items():
try:
key = _make_key(self.category, name)
_internal_kv_put(key, serialized, overwrite=True)
logger.debug(
f"Flushed pending registration for {self.category} '{name}'"
)
except Exception as e:
logger.warning(
f"Failed to flush {self.category} '{name}': {e}", exc_info=True
)
self._pending.clear()
# Global registry instances for different component categories
_registries: dict[str, ComponentRegistry] = {}
def get_registry(category: str) -> ComponentRegistry:
"""Get or create a registry for a component category.
Args:
category: The component category name
Returns:
The ComponentRegistry instance for this category
"""
if category not in _registries:
_registries[category] = ComponentRegistry(category)
return _registries[category]
def _flush_all_registries():
"""Flush all pending registrations to KV store.
This is registered as a Ray post-init hook to ensure registrations
made before Ray initialization are available across processes.
"""
for registry in _registries.values():
registry.flush_pending()
if _flush_all_registries not in worker._post_init_hooks:
worker._post_init_hooks.append(_flush_all_registries)