Files
lmcache--lmcache/lmcache/v1/cache_controller/executor.py
T
2026-07-13 12:24:33 +08:00

464 lines
16 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
from typing import Union
import asyncio
import uuid
# Third Party
import msgspec
import zmq.asyncio
# First Party
from lmcache.logging import init_logger
from lmcache.v1.cache_controller.message import ( # noqa: E501
CheckFinishMsg,
CheckFinishRetMsg,
ClearMsg,
ClearRetMsg,
ClearWorkerMsg,
CompressMsg,
CompressRetMsg,
CompressWorkerMsg,
DecompressMsg,
DecompressRetMsg,
DecompressWorkerMsg,
ErrorMsg,
HealthMsg,
HealthRetMsg,
HealthWorkerMsg,
HealthWorkerRetMsg,
MoveMsg,
MoveRetMsg,
MoveWorkerMsg,
Msg,
MsgBase,
PinMsg,
PinRetMsg,
PinWorkerMsg,
)
logger = init_logger(__name__)
# NOTE (Jiayi): `LMCacheClusterExecutor` might need to be in different processes
# in the future for the sake of performance.
# NOTE (Jiayi): Also, consider scaling up the number of cluster executors
# in the future.
# TODO (Jiayi): need better error handling
class LMCacheClusterExecutor:
"""
LMCache Cluster Executor class to handle the execution of cache operations.
"""
def __init__(self, reg_controller):
"""
Initialize the LMCache Executor with a cache instance.
:param lmcache_instance_id: lmcache_instance_id
"""
self.reg_controller = reg_controller
async def clear(self, msg: ClearMsg) -> Union[ClearRetMsg, ErrorMsg]:
"""
Execute a clear cache operation with error handling.
"""
instance_id = msg.instance_id
location = msg.location
worker_ids = self.reg_controller.get_workers(instance_id)
assert worker_ids is not None
sockets = []
serialized_msgs = []
for worker_id in worker_ids:
socket = self.reg_controller.get_socket(instance_id, worker_id)
if socket is None:
return ErrorMsg(
error=(
f"Worker {worker_id} not registered for instance {instance_id}"
)
)
sockets.append(socket)
# TODO(Jiayi): Need a way to track event_id -> worker_event_id mapping
# Also, we need to track worker_event_id status
worker_event_id = f"Worker{worker_id}{msg.event_id}"
serialized_msg = msgspec.msgpack.encode(
ClearWorkerMsg(
worker_event_id=worker_event_id,
location=location,
)
)
serialized_msgs.append(serialized_msg)
serialized_results = await self.execute_workers(
sockets=sockets,
serialized_msgs=serialized_msgs,
)
num_tokens_list = []
for i, serialized_result in enumerate(serialized_results):
result = msgspec.msgpack.decode(serialized_result, type=Msg)
num_tokens_list.append(result.num_tokens)
# TODO(Jiayi): Need to ensure cache consistency across workers.
assert len(set(num_tokens_list)) == 1, (
"The number of tokens cleared should be the same across all workers."
)
return ClearRetMsg(event_id=msg.event_id, num_tokens=num_tokens_list[0])
async def pin(self, msg: PinMsg) -> Union[PinRetMsg, ErrorMsg]:
"""
Execute a pin cache operation with error handling.
"""
instance_id = msg.instance_id
tokens = msg.tokens
location = msg.location
worker_ids = self.reg_controller.get_workers(instance_id)
assert worker_ids is not None
sockets = []
serialized_msgs = []
for worker_id in worker_ids:
socket = self.reg_controller.get_socket(instance_id, worker_id)
if socket is None:
return ErrorMsg(
error=(
f"Worker {worker_id} not registered for instance {instance_id}"
)
)
sockets.append(socket)
# TODO(Jiayi): Need a way to track event_id -> worker_event_id mapping
# Also, we need to track worker_event_id status
worker_event_id = f"Worker{worker_id}{msg.event_id}"
serialized_msg = msgspec.msgpack.encode(
PinWorkerMsg(
worker_event_id=worker_event_id,
tokens=tokens,
location=location,
)
)
serialized_msgs.append(serialized_msg)
serialized_results = await self.execute_workers(
sockets=sockets,
serialized_msgs=serialized_msgs,
)
num_tokens_list = []
for i, serialized_result in enumerate(serialized_results):
result = msgspec.msgpack.decode(serialized_result, type=Msg)
num_tokens_list.append(result.num_tokens)
# TODO(Jiayi): Need to ensure cache consistency across workers.
assert len(set(num_tokens_list)) == 1, (
"The number of tokens pinned should be the same across all workers."
)
return PinRetMsg(event_id=msg.event_id, num_tokens=num_tokens_list[0])
async def compress(self, msg: CompressMsg) -> Union[CompressRetMsg, ErrorMsg]:
"""
Execute a compress operation with error handling.
"""
event_id = msg.event_id
instance_id = msg.instance_id
method = msg.method
location = msg.location
tokens = msg.tokens
worker_ids = self.reg_controller.get_workers(instance_id)
assert worker_ids is not None
# TODO(Jiayi): Currently, we do not support PP or heterogeneous TP.
# NOTE(Jiayi): The TP ranks are already sorted in registration_controller.
sockets = []
serialized_msgs = []
for worker_id in worker_ids:
socket = self.reg_controller.get_socket(instance_id, worker_id)
if socket is None:
return ErrorMsg(
error=(
f"Worker {worker_id} not registered for "
f"instance {instance_id} or "
)
)
sockets.append(socket)
worker_event_id = f"CompressWorker{worker_id}{str(uuid.uuid4())}"
serialized_msg = msgspec.msgpack.encode(
CompressWorkerMsg(
worker_event_id=worker_event_id,
method=method,
location=location,
tokens=tokens,
)
)
serialized_msgs.append(serialized_msg)
logger.debug(
f"Sending compress operation to worker ({instance_id}, {worker_id})"
)
serialized_results = await self.execute_workers(
sockets=sockets,
serialized_msgs=serialized_msgs,
)
num_tokens_list = []
for serialized_result in serialized_results:
result = msgspec.msgpack.decode(serialized_result, type=Msg)
num_tokens_list.append(result.num_tokens)
# TODO(Jiayi): Need to ensure cache consistency across workers.
assert len(set(num_tokens_list)) == 1, (
"The number of tokens compressed should be the same across all workers."
)
return CompressRetMsg(
event_id=event_id,
num_tokens=num_tokens_list[0],
)
async def decompress(self, msg: DecompressMsg) -> Union[DecompressRetMsg, ErrorMsg]:
"""
Execute a decompress operation with error handling.
"""
event_id = msg.event_id
instance_id = msg.instance_id
method = msg.method
location = msg.location
tokens = msg.tokens
worker_ids = self.reg_controller.get_workers(instance_id)
assert worker_ids is not None
sockets = []
serialized_msgs = []
for worker_id in worker_ids:
socket = self.reg_controller.get_socket(instance_id, worker_id)
if socket is None:
return ErrorMsg(
error=(
f"Worker {worker_id} not registered for "
f"instance {instance_id} or "
)
)
sockets.append(socket)
worker_event_id = f"DecompressWorker{worker_id}{str(uuid.uuid4())}"
serialized_msg = msgspec.msgpack.encode(
DecompressWorkerMsg(
worker_event_id=worker_event_id,
method=method,
location=location,
tokens=tokens,
)
)
serialized_msgs.append(serialized_msg)
logger.debug(
f"Sending decompress operation to worker ({instance_id}, {worker_id})"
)
serialized_results = await self.execute_workers(
sockets=sockets,
serialized_msgs=serialized_msgs,
)
num_tokens_list = []
for serialized_result in serialized_results:
result = msgspec.msgpack.decode(serialized_result, type=Msg)
num_tokens_list.append(result.num_tokens)
assert len(set(num_tokens_list)) == 1, (
"The number of tokens decompressed should be the same across all workers."
)
return DecompressRetMsg(
event_id=event_id,
num_tokens=num_tokens_list[0],
)
async def move(self, msg: MoveMsg) -> Union[MoveRetMsg, ErrorMsg]:
"""
Execute a move cache operation with error handling.
"""
# NOTE(Jiayi): Currently we assume the transfer is push-based.
src_instance_id = msg.old_position[0]
dst_instance_id = msg.new_position[0]
src_worker_ids = self.reg_controller.get_workers(src_instance_id)
assert src_worker_ids is not None
dst_worker_ids = self.reg_controller.get_workers(dst_instance_id)
assert dst_worker_ids is not None
# TODO(Jiayi): Currently, we do not support PP or heterogeneous TP.
# NOTE(Jiayi): The TP ranks are already sorted in registration_controller.
sockets = []
serialized_msgs = []
for src_worker_id, dst_worker_id in zip(
src_worker_ids, dst_worker_ids, strict=False
):
socket = self.reg_controller.get_socket(src_instance_id, src_worker_id)
dst_url = self.reg_controller.get_peer_init_url(
dst_instance_id, dst_worker_id
)
if socket is None or dst_url is None:
return ErrorMsg(
error=(
f"Src worker {src_worker_id} not registered for "
f"instance {src_instance_id} or "
f"dst worker {dst_worker_id} not registered for "
f"instance {dst_instance_id} or P2P is not enabled."
)
)
sockets.append(socket)
worker_event_id = f"MoveWorker{src_worker_id}{str(uuid.uuid4())}"
serialized_msg = msgspec.msgpack.encode(
MoveWorkerMsg(
worker_event_id=worker_event_id,
old_position=msg.old_position[1],
new_position=(dst_url, msg.new_position[1]),
tokens=msg.tokens,
copy=msg.copy,
)
)
serialized_msgs.append(serialized_msg)
logger.debug(
f"Sending move operation to worker ({src_instance_id}, {src_worker_id})"
)
serialized_results = await self.execute_workers(
sockets=sockets,
serialized_msgs=serialized_msgs,
)
num_tokens_list = []
for serialized_result in serialized_results:
result = msgspec.msgpack.decode(serialized_result, type=Msg)
num_tokens_list.append(result.num_tokens)
# TODO(Jiayi): Need to ensure cache consistency across workers.
assert len(set(num_tokens_list)) == 1, (
"The number of tokens moved should be the same across all workers."
)
return MoveRetMsg(
event_id=msg.event_id,
num_tokens=num_tokens_list[0],
)
async def health(self, msg: HealthMsg) -> Union[HealthRetMsg, ErrorMsg]:
"""
Execute a compress operation with error handling.
"""
instance_id = msg.instance_id
worker_ids = self.reg_controller.get_workers(instance_id)
if worker_ids is None:
return ErrorMsg(error=f"No workers found for instance {instance_id}")
# TODO(Jiayi): Currently, we do not support PP or heterogeneous TP.
# NOTE(Jiayi): The TP ranks are already sorted in registration_controller.
sockets = []
serialized_msgs = []
for worker_id in worker_ids:
socket = self.reg_controller.get_socket(instance_id, worker_id)
if socket is None:
return ErrorMsg(
error=(
f"Worker {worker_id} not registered for "
f"instance {instance_id} or socket not found"
)
)
sockets.append(socket)
worker_event_id = f"HealthWorker{worker_id}{str(uuid.uuid4())}"
serialized_msg = msgspec.msgpack.encode(
HealthWorkerMsg(
worker_event_id=worker_event_id,
)
)
serialized_msgs.append(serialized_msg)
logger.debug(
f"Sending health check operation to worker ({instance_id}, {worker_id})"
)
# Collect results from all workers
serialized_results = await self.execute_workers(
sockets=sockets,
serialized_msgs=serialized_msgs,
)
# Process results
error_codes = {}
for i, serialized_result in enumerate(serialized_results):
try:
result = msgspec.msgpack.decode(serialized_result, type=Msg)
if isinstance(result, HealthWorkerRetMsg):
error_codes[worker_ids[i]] = result.error_code
elif isinstance(result, ErrorMsg):
error_codes[worker_ids[i]] = -1001 # Worker returned error
else:
error_codes[worker_ids[i]] = -1002 # Unexpected response
except Exception as e:
logger.error(
f"Failed to parse health response from worker "
f"{worker_ids[i]}: {str(e)}"
)
error_codes[worker_ids[i]] = -1003 # Failed to parse response
return HealthRetMsg(
event_id=msg.event_id,
error_codes=error_codes,
)
async def check_finish(
self, msg: CheckFinishMsg
) -> Union[CheckFinishRetMsg, ErrorMsg]:
raise NotImplementedError
# TODO(Jiayi): need to make the types more specific
async def execute(self, operation: str, msg: MsgBase) -> MsgBase:
"""
Execute a cache operation with error handling.
:param operation: The operation to execute
(e.g., 'clear').
:param msg: The message containing the operation details.
:return: The result of the operation or an error message.
"""
try:
method = getattr(self, operation)
return await method(msg)
except AttributeError:
return ErrorMsg(error=f"Operation '{operation}' is not supported.")
except Exception as e:
return ErrorMsg(error=str(e))
async def execute_workers(
self,
sockets: list[zmq.asyncio.Socket],
serialized_msgs: list[bytes],
) -> list[bytes]:
"""
Execute a list of serialized messages on the given sockets.
:param sockets: The list of sockets to send the messages to.
:param serialized_msgs: The list of serialized messages to send.
:return: A list of serialized results received from the sockets.
"""
tasks = []
for socket, serialized_msg in zip(sockets, serialized_msgs, strict=False):
async def send_and_receive(s, msg):
await s.send(msg)
return await s.recv()
tasks.append(send_and_receive(socket, serialized_msg))
serialized_results = await asyncio.gather(*tasks)
return serialized_results