mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
feat(batch_redis_get.py): batch redis GET requests for a given key + call type
reduces the number of GET requests we're making in high-throughput scenarios
This commit is contained in:
parent
e033e84720
commit
226953e1d8
5 changed files with 189 additions and 5 deletions
|
|
@ -129,6 +129,16 @@ class RedisCache(BaseCache):
|
|||
f"LiteLLM Caching: set() - Got exception from REDIS : {str(e)}"
|
||||
)
|
||||
|
||||
async def async_scan_iter(self, pattern: str, count: int = 100) -> list:
|
||||
keys = []
|
||||
_redis_client = self.init_async_client()
|
||||
async with _redis_client as redis_client:
|
||||
async for key in redis_client.scan_iter(match=pattern + "*", count=count):
|
||||
keys.append(key)
|
||||
if len(keys) >= count:
|
||||
break
|
||||
return keys
|
||||
|
||||
async def async_set_cache(self, key, value, **kwargs):
|
||||
_redis_client = self.init_async_client()
|
||||
async with _redis_client as redis_client:
|
||||
|
|
@ -172,8 +182,6 @@ class RedisCache(BaseCache):
|
|||
return results
|
||||
except Exception as e:
|
||||
print_verbose(f"Error occurred in pipeline write - {str(e)}")
|
||||
# NON blocking - notify users Redis is throwing an exception
|
||||
logging.debug("LiteLLM Caching: set() - Got exception from REDIS : ", e)
|
||||
|
||||
def _get_cache_logic(self, cached_response: Any):
|
||||
"""
|
||||
|
|
@ -220,6 +228,36 @@ class RedisCache(BaseCache):
|
|||
traceback.print_exc()
|
||||
logging.debug("LiteLLM Caching: get() - Got exception from REDIS: ", e)
|
||||
|
||||
async def async_get_cache_pipeline(self, key_list) -> dict:
|
||||
"""
|
||||
Use Redis for bulk read operations
|
||||
"""
|
||||
_redis_client = await self.init_async_client()
|
||||
key_value_dict = {}
|
||||
try:
|
||||
async with _redis_client as redis_client:
|
||||
async with redis_client.pipeline(transaction=True) as pipe:
|
||||
# Queue the get operations in the pipeline for all keys.
|
||||
for cache_key in key_list:
|
||||
pipe.get(cache_key) # Queue GET command in pipeline
|
||||
|
||||
# Execute the pipeline and await the results.
|
||||
results = await pipe.execute()
|
||||
|
||||
# Associate the results back with their keys.
|
||||
# 'results' is a list of values corresponding to the order of keys in 'key_list'.
|
||||
key_value_dict = dict(zip(key_list, results))
|
||||
|
||||
decoded_results = {
|
||||
k.decode("utf-8"): self._get_cache_logic(v)
|
||||
for k, v in key_value_dict.items()
|
||||
}
|
||||
|
||||
return decoded_results
|
||||
except Exception as e:
|
||||
print_verbose(f"Error occurred in pipeline read - {str(e)}")
|
||||
return key_value_dict
|
||||
|
||||
def flush_cache(self):
|
||||
self.redis_client.flushall()
|
||||
|
||||
|
|
@ -1001,6 +1039,10 @@ class Cache:
|
|||
if self.namespace is not None:
|
||||
hash_hex = f"{self.namespace}:{hash_hex}"
|
||||
print_verbose(f"Hashed Key with Namespace: {hash_hex}")
|
||||
elif kwargs.get("metadata", {}).get("redis_namespace", None) is not None:
|
||||
_namespace = kwargs.get("metadata", {}).get("redis_namespace", None)
|
||||
hash_hex = f"{_namespace}:{hash_hex}"
|
||||
print_verbose(f"Hashed Key with Namespace: {hash_hex}")
|
||||
return hash_hex
|
||||
|
||||
def generate_streaming_content(self, content):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ model_list:
|
|||
model: gpt-3.5-turbo-1106
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
litellm_settings:
|
||||
cache: true
|
||||
cache_params:
|
||||
type: redis
|
||||
# callbacks: ["batch_redis_requests"]
|
||||
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
database_url: "postgresql://krrishdholakia:9yQkKWiB8vVs@ep-icy-union-a5j4dwls.us-east-2.aws.neon.tech/neondb?sslmode=require"
|
||||
# database_url: "postgresql://krrishdholakia:9yQkKWiB8vVs@ep-icy-union-a5j4dwls.us-east-2.aws.neon.tech/neondb?sslmode=require"
|
||||
124
litellm/proxy/hooks/batch_redis_get.py
Normal file
124
litellm/proxy/hooks/batch_redis_get.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# What this does?
|
||||
## Gets a key's redis cache, and store it in memory for 1 minute.
|
||||
## This reduces the number of REDIS GET requests made during high-traffic by the proxy.
|
||||
### [BETA] this is in Beta. And might change.
|
||||
|
||||
from typing import Optional, Literal
|
||||
import litellm
|
||||
from litellm.caching import DualCache, RedisCache, InMemoryCache
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from fastapi import HTTPException
|
||||
import json, traceback
|
||||
|
||||
|
||||
class _PROXY_BatchRedisRequests(CustomLogger):
|
||||
# Class variables or attributes
|
||||
in_memory_cache: Optional[InMemoryCache] = None
|
||||
|
||||
def __init__(self):
|
||||
litellm.cache.async_get_cache = (
|
||||
self.async_get_cache
|
||||
) # map the litellm 'get_cache' function to our custom function
|
||||
|
||||
def print_verbose(
|
||||
self, print_statement, debug_level: Literal["INFO", "DEBUG"] = "DEBUG"
|
||||
):
|
||||
if debug_level == "DEBUG":
|
||||
verbose_proxy_logger.debug(print_statement)
|
||||
elif debug_level == "INFO":
|
||||
verbose_proxy_logger.debug(print_statement)
|
||||
if litellm.set_verbose is True:
|
||||
print(print_statement) # noqa
|
||||
|
||||
async def async_pre_call_hook(
|
||||
self,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
cache: DualCache,
|
||||
data: dict,
|
||||
call_type: str,
|
||||
):
|
||||
try:
|
||||
"""
|
||||
Get the user key
|
||||
|
||||
Check if a key starting with `litellm:<api_key>:<call_type:` exists in-memory
|
||||
|
||||
If no, then get relevant cache from redis
|
||||
"""
|
||||
api_key = user_api_key_dict.api_key
|
||||
|
||||
cache_key_name = f"litellm:{api_key}:{call_type}"
|
||||
self.in_memory_cache = cache.in_memory_cache
|
||||
|
||||
key_value_dict = {}
|
||||
in_memory_cache_exists = False
|
||||
for key in cache.in_memory_cache.cache_dict.keys():
|
||||
if isinstance(key, str) and key.startswith(cache_key_name):
|
||||
in_memory_cache_exists = True
|
||||
|
||||
if in_memory_cache_exists == False and litellm.cache is not None:
|
||||
"""
|
||||
- Check if `litellm.Cache` is redis
|
||||
- Get the relevant values
|
||||
"""
|
||||
if litellm.cache.type is not None and isinstance(
|
||||
litellm.cache.cache, RedisCache
|
||||
):
|
||||
# Initialize an empty list to store the keys
|
||||
keys = []
|
||||
self.print_verbose(f"cache_key_name: {cache_key_name}")
|
||||
# Use the SCAN iterator to fetch keys matching the pattern
|
||||
keys = await litellm.cache.cache.async_scan_iter(
|
||||
pattern=cache_key_name, count=100
|
||||
)
|
||||
# If you need the truly "last" based on time or another criteria,
|
||||
# ensure your key naming or storage strategy allows this determination
|
||||
# Here you would sort or filter the keys as needed based on your strategy
|
||||
self.print_verbose(f"redis keys: {keys}")
|
||||
if len(keys) > 0:
|
||||
key_value_dict = (
|
||||
await litellm.cache.cache.async_get_cache_pipeline(
|
||||
key_list=keys
|
||||
)
|
||||
)
|
||||
|
||||
## Add to cache
|
||||
for key, value in key_value_dict.items():
|
||||
_cache_key = f"{cache_key_name}:{key}"
|
||||
cache.in_memory_cache.cache_dict[_cache_key] = value
|
||||
|
||||
## Set cache namespace if it's a miss
|
||||
data["metadata"]["redis_namespace"] = cache_key_name
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
||||
async def async_get_cache(self, *args, **kwargs):
|
||||
"""
|
||||
- Check if the cache key is in-memory
|
||||
|
||||
- Else return None
|
||||
"""
|
||||
try: # never block execution
|
||||
if "cache_key" in kwargs:
|
||||
cache_key = kwargs["cache_key"]
|
||||
else:
|
||||
cache_key = litellm.cache.get_cache_key(
|
||||
*args, **kwargs
|
||||
) # returns "<cache_key_name>:<hash>" - we pass redis_namespace in async_pre_call_hook. Done to avoid rewriting the async_set_cache logic
|
||||
if cache_key is not None and self.in_memory_cache is not None:
|
||||
cache_control_args = kwargs.get("cache", {})
|
||||
max_age = cache_control_args.get(
|
||||
"s-max-age", cache_control_args.get("s-maxage", float("inf"))
|
||||
)
|
||||
cached_result = self.in_memory_cache.get_cache(
|
||||
cache_key, *args, **kwargs
|
||||
)
|
||||
return litellm.cache._get_cache_logic(
|
||||
cached_result=cached_result, max_age=max_age
|
||||
)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
|
@ -1795,6 +1795,16 @@ class ProxyConfig:
|
|||
_ENTERPRISE_PromptInjectionDetection()
|
||||
)
|
||||
imported_list.append(prompt_injection_detection_obj)
|
||||
elif (
|
||||
isinstance(callback, str)
|
||||
and callback == "batch_redis_requests"
|
||||
):
|
||||
from litellm.proxy.hooks.batch_redis_get import (
|
||||
_PROXY_BatchRedisRequests,
|
||||
)
|
||||
|
||||
batch_redis_obj = _PROXY_BatchRedisRequests()
|
||||
imported_list.append(batch_redis_obj)
|
||||
else:
|
||||
imported_list.append(
|
||||
get_instance_fn(
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ from .integrations.litedebugger import LiteDebugger
|
|||
from .proxy._types import KeyManagementSystem
|
||||
from openai import OpenAIError as OriginalError
|
||||
from openai._models import BaseModel as OpenAIObject
|
||||
from .caching import S3Cache, RedisSemanticCache
|
||||
from .caching import S3Cache, RedisSemanticCache, RedisCache
|
||||
from .exceptions import (
|
||||
AuthenticationError,
|
||||
BadRequestError,
|
||||
|
|
@ -2806,7 +2806,9 @@ def client(original_function):
|
|||
):
|
||||
if len(cached_result) == 1 and cached_result[0] is None:
|
||||
cached_result = None
|
||||
elif isinstance(litellm.cache.cache, RedisSemanticCache):
|
||||
elif isinstance(
|
||||
litellm.cache.cache, RedisSemanticCache
|
||||
) or isinstance(litellm.cache.cache, RedisCache):
|
||||
preset_cache_key = litellm.cache.get_cache_key(*args, **kwargs)
|
||||
kwargs["preset_cache_key"] = (
|
||||
preset_cache_key # for streaming calls, we need to pass the preset_cache_key
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue