Files
2026-07-13 12:24:33 +08:00

898 lines
31 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
from typing import Annotated, Any, Callable, List, Optional, Tuple
import asyncio
import hashlib
import json
import traceback
# Third Party
from fastapi import APIRouter, Query
from starlette.requests import Request
from starlette.responses import PlainTextResponse
import torch
# First Party
from lmcache.logging import init_logger
from lmcache.utils import (
compress_slot_mapping,
parse_mixed_slot_mapping,
)
from lmcache.v1.cache_engine import LMCacheEngine
logger = init_logger(__name__)
router = APIRouter()
def _parse_tokens_from_params(
tokens_mock: Optional[str],
) -> Tuple[Optional[List[int]], Optional[dict]]:
"""Parse tokens from input parameters.
Args:
tokens_mock: Two comma-separated numbers specifying start and end of token range
- Example: "0,100" generates tokens [0, 1, 2, ..., 99]
- Example: "50,75" generates tokens [50, 51, 52, ..., 74]
Returns:
Tuple of (tokens list, error dict).
If error dict is not None, tokens will be None.
"""
# TODO(baoloongmao): Add support for tokens_input parameter to read tokens from file
if tokens_mock:
try:
parts = tokens_mock.split(",")
if len(parts) != 2:
raise ValueError("tokens_mock must contain exactly 2 numbers")
start, end = int(parts[0].strip()), int(parts[1].strip())
if start >= end:
raise ValueError("start must be less than end")
tokens = list(range(start, end))
return tokens, None
except ValueError as e:
return None, {
"error": "Invalid tokens_mock format",
"message": f"tokens_mock must be 'start,end': {str(e)}",
}
else:
return None, {
"error": "Missing parameters",
"message": "Must specify either tokens_input or tokens_mock",
}
def _create_error_response(error_info: dict, status_code: int) -> PlainTextResponse:
"""Create a standardized error response.
Args:
error_info: Dictionary containing error information
status_code: HTTP status code
Returns:
PlainTextResponse with error information
"""
return PlainTextResponse(
content=json.dumps(error_info, indent=2),
media_type="application/json",
status_code=status_code,
)
def _check_lmcache_engine(
request: Request,
) -> Tuple[Optional["LMCacheEngine"], Optional[PlainTextResponse]]:
"""Check if LMCache engine is available.
Args:
request: FastAPI request object
Returns:
Tuple of (lmcache_engine, error_response).
If error_response is not None, engine will be None.
"""
lmcache_adapter = request.app.state.lmcache_adapter
lmcache_engine = getattr(lmcache_adapter, "lmcache_engine", None)
if not lmcache_engine:
error_info = {
"error": "LMCache API is unavailable",
"message": "LMCache engine not configured.",
}
return None, _create_error_response(error_info, 503)
return lmcache_engine, None
def _get_kvcaches_and_device(engine):
"""Get kvcaches and device from engine's gpu_connector.
Args:
engine: LMCache engine instance
Returns:
Tuple of (kvcaches, device).
kvcaches may be None if not available.
device defaults to "cpu" if kvcaches not available.
"""
kvcaches = None
device = "cpu" # Default device
if engine.gpu_connector:
kvcaches = engine.gpu_connector.kvcaches
if kvcaches is not None and len(kvcaches) > 0:
device = kvcaches[0].device
logger.debug("Using kvcaches device: %s", device)
else:
logger.warning(
"gpu_connector.kvcaches is None or empty. "
"Make sure post_init was called with kvcaches."
)
return kvcaches, device
def _compute_tensor_checksum(tensor: torch.Tensor) -> str:
"""Compute MD5 checksum of a tensor."""
# Move to CPU and convert to bytes for hashing
# Handle BFloat16 which is not supported by numpy
if tensor.dtype == torch.bfloat16:
# Convert bfloat16 to float32 for numpy compatibility
tensor = tensor.to(torch.float32)
tensor_bytes = tensor.detach().cpu().contiguous().numpy().tobytes()
return hashlib.md5(tensor_bytes).hexdigest()
def _slice_by_slot_dim(
kv_at_slots: torch.Tensor, start_idx: int, end_idx: int
) -> torch.Tensor:
"""Slice tensor by slot dimension based on tensor ndim."""
if kv_at_slots.ndim == 4:
# MHA: [2, num_slots, num_heads, head_size]
return kv_at_slots[:, start_idx:end_idx, :, :]
elif kv_at_slots.ndim == 3:
# 4D format: [num_slots, num_heads, head_size]
return kv_at_slots[start_idx:end_idx, :, :]
else:
# MLA: [num_slots, head_size]
return kv_at_slots[start_idx:end_idx, :]
def _extract_kv_at_slots(
kv_tensor: torch.Tensor, slot_tensor: torch.Tensor
) -> torch.Tensor:
"""Extract KV data at specified slot positions from kv_tensor.
Handles different kv_tensor formats:
- MHA (5D): [2, num_blocks, block_size, num_heads, head_size]
- MLA (3D): [num_blocks, block_size, head_size]
The slot_mapping is calculated as:
slot_idx = block_id * block_size + block_offset
This means we can reshape the tensor to flatten (num_blocks, block_size)
into a single slot dimension and index directly.
Args:
kv_tensor: The KV cache tensor for a single layer.
slot_tensor: Tensor of slot indices to extract.
Returns:
Tensor with KV data at the specified slots.
- MHA: shape [2, num_slots, num_heads, head_size]
- MLA: shape [num_slots, head_size]
"""
ndim = kv_tensor.ndim
if ndim == 5:
# MHA format: [2, num_blocks, block_size, num_heads, head_size]
# Reshape to [2, num_blocks * block_size, num_heads, head_size]
# then index by slot_tensor on dimension 1
kv_2d = 2
num_heads = kv_tensor.shape[3]
head_size = kv_tensor.shape[4]
kv_reshaped = kv_tensor.reshape(kv_2d, -1, num_heads, head_size)
return kv_reshaped[:, slot_tensor, :, :]
elif ndim == 3:
# MLA format: [num_blocks, block_size, head_size]
# Reshape to [num_blocks * block_size, head_size]
head_size = kv_tensor.shape[2]
kv_reshaped = kv_tensor.reshape(-1, head_size)
return kv_reshaped[slot_tensor, :]
elif ndim == 4:
# Alternative format: [num_blocks, block_size, num_heads, head_size]
# (used in some test cases)
num_heads = kv_tensor.shape[2]
head_size = kv_tensor.shape[3]
kv_reshaped = kv_tensor.reshape(-1, num_heads, head_size)
return kv_reshaped[slot_tensor, :, :]
else:
# Fallback: try the original approach
logger.warning(
"Unknown kv_tensor ndim=%d, shape=%s. Using fallback indexing.",
ndim,
kv_tensor.shape,
)
return kv_tensor.view(-1, *kv_tensor.shape[2:])[slot_tensor]
def compute_kvcache_checksums(
lmcache_adapter,
slot_indices: list[int],
chunk_size: Optional[int] = None,
layerwise: bool = False,
) -> Optional[dict[str, Any]]:
"""Compute MD5 checksums for kvcaches at specified slot positions.
This method is used by the kvcache check API to verify that stored
and retrieved kvcaches are identical.
The slot_mapping is calculated in vllm_v1_adapter.py as:
slot_idx = block_id * block_size + block_offset
For vLLM kv_cache formats:
- MHA (5D): [2, num_blocks, block_size, num_heads, head_size]
- MLA (3D): [num_blocks, block_size, head_size]
Args:
lmcache_adapter: The LMCache adapter containing kv_caches.
slot_indices: List of slot indices to compute checksums for.
chunk_size: Optional chunk size for computing per-chunk checksums.
If provided, will compute checksums for each chunk.
layerwise: If True, output per-layer checksums for each chunk.
If False (default), output one checksum per chunk (all layers combined).
Returns:
Dictionary containing:
- 'chunk_checksums': (if layerwise=True) dict mapping layer names to
list of per-chunk checksums
- 'chunk_checksums': (if layerwise=False) list of checksums, one per chunk
(each checksum covers all layers for that chunk)
Returns None if kv_caches is not available.
"""
if not lmcache_adapter.kv_caches:
logger.warning("kv_caches is empty, cannot compute checksums")
return None
if chunk_size is None or chunk_size <= 0:
return {"chunk_checksums": [], "chunk_size": chunk_size, "num_chunks": 0}
num_slots = len(slot_indices)
num_chunks = (num_slots + chunk_size - 1) // chunk_size
# Pre-extract all layer data at slot positions
layer_data_at_slots: dict[str, torch.Tensor] = {}
for layer_name, kv_tensor in lmcache_adapter.kv_caches.items():
try:
slot_tensor = torch.tensor(
slot_indices, dtype=torch.long, device=kv_tensor.device
)
layer_data_at_slots[layer_name] = _extract_kv_at_slots(
kv_tensor, slot_tensor
)
except Exception as e:
logger.error("Failed to extract data for layer %s: %s", layer_name, str(e))
layer_data_at_slots[layer_name] = None # type: ignore
if layerwise:
# Output per-layer checksums for each chunk: {layer_name: [checksum1, ...]}
chunk_checksums: dict[str, list[str]] = {}
for layer_name, kv_at_slots in layer_data_at_slots.items():
if kv_at_slots is None:
chunk_checksums[layer_name] = ["error"] * num_chunks
continue
chunk_checksum_list: list[str] = []
for chunk_idx in range(num_chunks):
start_idx = chunk_idx * chunk_size
end_idx = min(start_idx + chunk_size, num_slots)
chunk_data = _slice_by_slot_dim(kv_at_slots, start_idx, end_idx)
chunk_checksum_list.append(_compute_tensor_checksum(chunk_data))
chunk_checksums[layer_name] = chunk_checksum_list
return {
"chunk_checksums": chunk_checksums,
"chunk_size": chunk_size,
"num_chunks": num_chunks,
}
else:
# Output one checksum per chunk (all layers combined): [checksum1, ...]
chunk_checksums_list: list[str] = []
for chunk_idx in range(num_chunks):
start_idx = chunk_idx * chunk_size
end_idx = min(start_idx + chunk_size, num_slots)
md5_hash = hashlib.md5()
for layer_name in sorted(layer_data_at_slots.keys()):
kv_at_slots = layer_data_at_slots[layer_name]
if kv_at_slots is None:
continue
chunk_data = _slice_by_slot_dim(kv_at_slots, start_idx, end_idx)
md5_hash.update(_compute_tensor_checksum(chunk_data).encode())
chunk_checksums_list.append(md5_hash.hexdigest())
return {
"chunk_checksums": chunk_checksums_list,
"chunk_size": chunk_size,
"num_chunks": num_chunks,
}
@router.delete("/cache/clear")
async def clear(
request: Request,
locations: Annotated[Optional[List[str]], Query()] = None,
request_configs: Optional[dict] = None,
):
"""Clear cached data from the LMCache engine.
This endpoint provides a way to clear cached KV (Key-Value) data from the
LMCache engine. It can clear all cached data or selectively clear data
from specific storage locations.
Args:
request (Request): The FastAPI request object containing application state.
locations (Optional[List[str]], optional): List of storage backend locations
to clear cache from. If None, clears from all available locations.
Common values include ["LocalCPUBackend", "LocalDiskBackend"].
Defaults to None.
request_configs (Optional[dict], optional): Additional configuration
parameters for the clear operation. Currently unused but reserved
for future extensions. Defaults to None.
Returns:
PlainTextResponse: A plain text response
Example:
Clear all cached data:
```bash
curl -X DELETE "http://localhost:8000/cache/clear"
# Response: {"status": "success", "num_removed": 10,
# "locations": null, "request_configs": null}
```
Clear cache from specific locations:
```bash
curl -X DELETE "http://localhost:8000/cache/clear?locations=LocalCPUBackend&locations=LocalDiskBackend"
# Response: {"status": "success", "num_removed": 5,
# "locations": ["LocalCPUBackend", "LocalDiskBackend"],
# "request_configs": null}
```
"""
try:
lmcache_engine, error_response = _check_lmcache_engine(request)
if error_response:
return error_response
assert lmcache_engine is not None
num_removed = lmcache_engine.clear( # type: ignore[attr-defined]
locations=locations, request_configs=request_configs
)
success_info = {
"status": "success",
"num_removed": num_removed,
}
return PlainTextResponse(
content=json.dumps(success_info, indent=2),
media_type="application/json",
)
except Exception as e:
error_info = {"error": "Failed to clear cache", "message": str(e)}
return _create_error_response(error_info, 500)
def _process_tokens_request(
request: Request,
tokens_mock: Optional[str],
) -> Tuple[Optional[object], Optional[List[int]], Optional[PlainTextResponse]]:
"""Process tokens request and validate parameters.
Args:
request: FastAPI request object
tokens_mock: Mock token range specification
Returns:
Tuple of (lmcache_engine, tokens, error_response).
If error_response is not None, the other values will be None.
"""
lmcache_engine, error_response = _check_lmcache_engine(request)
if error_response:
return None, None, error_response
tokens, error_info = _parse_tokens_from_params(tokens_mock)
if error_info:
status_code = 400 if error_info["error"] != "File not found" else 404
return None, None, _create_error_response(error_info, status_code)
return lmcache_engine, tokens, None
def _execute_cache_operation(
operation_name: str,
operation_func: Callable,
lmcache_engine: object,
tokens: List[int],
) -> PlainTextResponse:
"""Execute a cache operation and return standardized response.
Args:
operation_name: Name of the operation for error messages
operation_func: Function to execute the operation
lmcache_engine: LMCache engine instance
tokens: List of token IDs
Returns:
PlainTextResponse with operation result
"""
try:
result = operation_func(lmcache_engine, tokens)
success_info = {
"status": "success",
"num_tokens": len(tokens),
}
if result is not None:
success_info.update(result)
return PlainTextResponse(
content=json.dumps(success_info, indent=2),
media_type="application/json",
)
except Exception as e:
# Log the full traceback for debugging
tb_str = traceback.format_exc()
logger.error("Failed to %s: %s\n%s", operation_name, e, tb_str)
# Include more detailed error info in response
error_message = str(e) if str(e) else f"Exception type: {type(e).__name__}"
error_info = {
"error": f"Failed to {operation_name}",
"message": error_message,
"exception_type": type(e).__name__,
}
return _create_error_response(error_info, 500)
@router.post("/cache/store")
async def store(
request: Request,
tokens_mock: Optional[str] = None,
):
"""Store KV cache data into the LMCache engine.
This endpoint provides a way to store KV cache data by generating mock tokens.
Args:
request (Request): The FastAPI request object containing application state.
tokens_mock (Optional[str], optional): Two comma-separated numbers specifying
the start and end of a token range. Example: "0,100" generates tokens
from 0 to 99. Defaults to None.
Returns:
PlainTextResponse: A plain text response with operation status
Example:
Store with mock tokens:
```bash
curl -X POST "http://localhost:8000/cache/store?tokens_mock=0,100"
# Response: {"status": "success", "num_tokens": 100}
```
"""
lmcache_engine, tokens, error_response = _process_tokens_request(
request, tokens_mock
)
if error_response:
return error_response
assert tokens is not None
assert lmcache_engine is not None
def _store_operation(engine, token_list):
# Get kvcaches and device using the shared function
kvcaches, device = _get_kvcaches_and_device(engine)
# Create slot mapping for the tokens
slot_mapping = torch.arange(len(token_list), dtype=torch.long, device=device)
logger.debug(
"Storing %d tokens with slot_mapping on device %s",
len(token_list),
device,
)
engine.store(
req_id="cache_api_store",
tokens=token_list,
slot_mapping=slot_mapping,
kvcaches=kvcaches,
)
return None
return _execute_cache_operation(
"store cache", _store_operation, lmcache_engine, tokens
)
@router.post("/cache/retrieve")
async def retrieve(
request: Request,
tokens_mock: Optional[str] = None,
):
"""Retrieve KV cache data from the LMCache engine.
This endpoint provides a way to retrieve KV cache data by generating mock tokens.
Args:
request (Request): The FastAPI request object containing application state.
tokens_mock (Optional[str], optional): Two comma-separated numbers specifying
the start and end of a token range. Example: "0,100" generates tokens
from 0 to 99. Defaults to None.
Returns:
PlainTextResponse: A plain text response with operation status
Example:
Retrieve with mock tokens:
```bash
curl -X POST "http://localhost:8000/cache/retrieve?tokens_mock=0,100"
# Response: {"status": "success", "num_tokens": 100, "num_retrieved": 80}
```
"""
lmcache_engine, tokens, error_response = _process_tokens_request(
request, tokens_mock
)
if error_response:
return error_response
assert tokens is not None
assert lmcache_engine is not None
def _retrieve_operation(engine, token_list):
# Get kvcaches and device using the shared function
kvcaches, device = _get_kvcaches_and_device(engine)
# Create slot_mapping for retrieve operation
slot_mapping = torch.arange(len(token_list), dtype=torch.long, device=device)
logger.debug(
"Retrieving %d tokens with slot_mapping on device %s",
len(token_list),
device,
)
ret_mask = engine.retrieve(
req_id="cache_api_retrieve",
tokens=token_list,
slot_mapping=slot_mapping,
kvcaches=kvcaches,
)
num_retrieved = int(ret_mask.sum().item())
return {"num_retrieved": num_retrieved}
return _execute_cache_operation(
"retrieve cache", _retrieve_operation, lmcache_engine, tokens
)
@router.get("/cache/kvcache/check")
async def kvcache_check(
request: Request,
slot_mapping: Optional[str] = None,
chunk_size: Optional[int] = None,
layerwise: bool = False,
):
"""Compute checksum for kvcaches at specified slot_mapping positions.
This endpoint is used to verify that stored and retrieved kvcaches are identical.
Args:
request (Request): The FastAPI request object containing application state.
slot_mapping (Optional[str], optional): Slot indices in comma-separated format,
supports single numbers and range expressions.
Examples: "0,1,2,3", "1,2,3,[9,12],17,19". Defaults to None.
chunk_size (Optional[int], optional): Chunk size for computing checksums.
Each chunk contains `chunk_size` slots. Required parameter.
layerwise (bool, optional): If True, output per-layer checksums for each chunk.
If False (default), output one checksum per chunk (all layers combined).
Returns:
PlainTextResponse: A JSON response containing checksums.
Example:
```bash
# layerwise=false (default): one checksum per chunk (all layers combined)
curl -X GET "http://localhost:8000/cache/kvcache/check?slot_mapping=0,1,2,3&chunk_size=2"
# Response: {
# "status": "success",
# "slot_mapping_ranges": [[0, 3]],
# "chunk_size": 2,
# "num_chunks": 2,
# "chunk_checksums": ["checksum_chunk0", "checksum_chunk1"],
# "layerwise": false
# }
# layerwise=true: per-layer checksums for each chunk
curl -X GET "http://localhost:8000/cache/kvcache/check?slot_mapping=0,1,2,3&chunk_size=2&layerwise=true"
# Response: {
# "status": "success",
# "slot_mapping_ranges": [[0, 3]],
# "chunk_size": 2,
# "num_chunks": 2,
# "chunk_checksums": {
# "layer_0": ["checksum_chunk0", "checksum_chunk1"],
# "layer_1": ["checksum_chunk0", "checksum_chunk1"],
# },
# "layerwise": true
# }
```
"""
try:
lmcache_adapter = request.app.state.lmcache_adapter
if not lmcache_adapter:
return _create_error_response(
{
"error": "LMCache adapter unavailable",
"message": "LMCache adapter not configured.",
},
503,
)
if not slot_mapping:
return _create_error_response(
{
"error": "Missing parameters",
"message": "slot_mapping parameter is required",
},
400,
)
# Parse slot_mapping from mixed format string
# (supports single numbers and ranges)
slot_indices, error_info = parse_mixed_slot_mapping(slot_mapping)
if error_info:
return _create_error_response(error_info, 400)
# slot_indices is guaranteed to be non-None when error_info is None
assert slot_indices is not None
# Validate slot indices are within valid range
if lmcache_adapter.kv_caches:
# Get the first kv_tensor to check dimensions
first_kv_tensor = next(iter(lmcache_adapter.kv_caches.values()))
# Calculate total slots: num_blocks * block_size
# For different formats:
# - MHA (5D): [2, num_blocks, block_size, num_heads, head_size]
# - MLA (3D): [num_blocks, block_size, head_size]
# - 4D: [num_blocks, block_size, num_heads, head_size]
ndim = first_kv_tensor.ndim
if ndim == 5:
# MHA: [2, num_blocks, block_size, num_heads, head_size]
total_slots = first_kv_tensor.shape[1] * first_kv_tensor.shape[2]
elif ndim == 3:
# MLA: [num_blocks, block_size, head_size]
total_slots = first_kv_tensor.shape[0] * first_kv_tensor.shape[1]
elif ndim == 4:
# 4D: [num_blocks, block_size, num_heads, head_size]
total_slots = first_kv_tensor.shape[0] * first_kv_tensor.shape[1]
else:
# Fallback
reshaped = first_kv_tensor.view(-1, *first_kv_tensor.shape[2:])
total_slots = reshaped.shape[0]
# Check each slot index
invalid_indices = []
for slot_idx in slot_indices:
if slot_idx < 0 or slot_idx >= total_slots:
invalid_indices.append(slot_idx)
if invalid_indices:
return _create_error_response(
{
"error": "Invalid slot indices",
"message": (
"Slot indices out of bounds: %s. Valid range: 0 to %d"
)
% (invalid_indices, total_slots - 1),
},
400,
)
else:
return _create_error_response(
{
"error": "kv_caches not available",
"message": "kv_caches is empty or not initialized",
},
404,
)
# Validate chunk_size if provided
if chunk_size is not None and chunk_size <= 0:
return _create_error_response(
{
"error": "Invalid chunk_size",
"message": "chunk_size must be a positive integer",
},
400,
)
# Get checksums from the adapter asynchronously to not block the loop
loop = asyncio.get_running_loop()
checksums_result = await loop.run_in_executor(
None, # Uses default ThreadPoolExecutor
lambda: compute_kvcache_checksums(
lmcache_adapter, slot_indices, chunk_size, layerwise
),
)
if checksums_result is None:
return _create_error_response(
{
"error": "Failed to compute checksums",
"message": "kv_caches not available or empty",
},
500,
)
# Compute slot mapping ranges using compress_slot_mapping
slot_mapping_ranges = compress_slot_mapping(slot_indices)
response_data: dict[str, Any] = {
"status": "success",
"slot_mapping_ranges": slot_mapping_ranges,
}
# Include chunk checksums
response_data["chunk_size"] = checksums_result.get("chunk_size")
response_data["num_chunks"] = checksums_result.get("num_chunks")
response_data["chunk_checksums"] = checksums_result.get("chunk_checksums")
response_data["layerwise"] = layerwise
return PlainTextResponse(
content=json.dumps(response_data, indent=2),
media_type="application/json",
)
except Exception as e:
logger.error("Failed to compute kvcache checksums: %s", str(e))
return _create_error_response(
{"error": "Failed to compute checksums", "message": str(e)},
500,
)
@router.post("/cache/kvcache/record_slot")
async def kvcache_record_slot(
request: Request,
enabled: Optional[str] = None,
):
"""Enable or disable KVCache Check slot_mapping logging.
This endpoint controls whether the KVCache Check logs (slot_mapping info)
are printed when store/retrieve operations are performed.
Args:
request (Request): The FastAPI request object containing application state.
enabled (Optional[str], optional): "true" to enable logging, "false" to
disable. Defaults to None.
Returns:
PlainTextResponse: A JSON response containing the current logging status.
Example:
```bash
# Enable logging
curl -X POST "http://localhost:8000/cache/kvcache/record_slot?enabled=true"
# Disable logging
curl -X POST "http://localhost:8000/cache/kvcache/record_slot?enabled=false"
# Check current status
curl -X POST "http://localhost:8000/cache/kvcache/record_slot"
```
"""
try:
lmcache_adapter = request.app.state.lmcache_adapter
if not lmcache_adapter:
return _create_error_response(
{
"error": "LMCache adapter unavailable",
"message": "LMCache adapter not configured.",
},
503,
)
# Get current status from lmcache_engine
lmcache_engine = lmcache_adapter.lmcache_engine
current_status = getattr(lmcache_engine, "kvcache_check_log_enabled", False)
# Update status if enabled parameter is provided
if enabled is not None:
enabled_lower = enabled.lower()
if enabled_lower == "true":
lmcache_engine.kvcache_check_log_enabled = True
current_status = True
logger.info("KVCache Check logging enabled")
elif enabled_lower == "false":
lmcache_engine.kvcache_check_log_enabled = False
current_status = False
logger.info("KVCache Check logging disabled")
else:
return _create_error_response(
{
"error": "Invalid parameter",
"message": "enabled must be 'true' or 'false'",
},
400,
)
response_data = {
"status": "success",
"kvcache_check_log_enabled": current_status,
}
return PlainTextResponse(
content=json.dumps(response_data, indent=2),
media_type="application/json",
)
except Exception as e:
logger.error("Failed to set kvcache record slot status: %s", str(e))
return _create_error_response(
{"error": "Failed to set record slot status", "message": str(e)},
500,
)
@router.get("/cache/kvcache/info")
async def kvcache_info(request: Request):
"""Get information about the current kvcaches.
Returns information about the kvcaches structure including layer names,
shapes, and device information.
Args:
request (Request): The FastAPI request object containing application state.
Returns:
PlainTextResponse: A JSON response containing kvcache information.
"""
try:
lmcache_adapter = request.app.state.lmcache_adapter
if not lmcache_adapter:
return _create_error_response(
{
"error": "LMCache adapter unavailable",
"message": "LMCache adapter not configured.",
},
503,
)
kv_caches = getattr(lmcache_adapter, "kvcaches", None)
if not kv_caches:
return _create_error_response(
{
"error": "kv_caches not available",
"message": "kv_caches is empty or not initialized",
},
404,
)
layers_info: dict = {}
for layer_name, kv_tensor in kv_caches.items():
layers_info[layer_name] = {
"shape": list(kv_tensor.shape),
"dtype": str(kv_tensor.dtype),
"device": str(kv_tensor.device),
}
info = {
"status": "success",
"num_layers": len(kv_caches),
"layers": layers_info,
}
return PlainTextResponse(
content=json.dumps(info, indent=2),
media_type="application/json",
)
except Exception as e:
logger.error("Failed to get kvcache info: %s", str(e))
return _create_error_response(
{"error": "Failed to get kvcache info", "message": str(e)},
500,
)