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

234 lines
7.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
AsyncSerdeProcessor: wraps sync Serializer/Deserializer into the async
SerdeProcessor interface expected by the controllers.
Runs serialization/deserialization tasks in a thread pool and signals
event notifiers on completion, matching the L2 adapter async pattern.
The notifiers come from :mod:`lmcache.v1.platform` so the same code
runs on Linux (eventfd) and other POSIX platforms (pipe fallback).
"""
# Standard
from concurrent.futures import ThreadPoolExecutor
import enum
import threading
# First Party
from lmcache.logging import init_logger
from lmcache.v1.distributed.api import MemoryLayoutDesc
from lmcache.v1.distributed.serde.base import (
Deserializer,
SerdeProcessor,
SerdeTaskId,
Serializer,
)
from lmcache.v1.memory_management import MemoryObj
from lmcache.v1.platform import create_event_notifier
logger = init_logger(__name__)
class _TaskType(enum.Enum):
SERIALIZE = enum.auto()
DESERIALIZE = enum.auto()
class AsyncSerdeProcessor(SerdeProcessor):
"""Wraps sync Serializer/Deserializer into async SerdeProcessor.
Runs each submitted task in a thread pool. On completion, stores
the result and signals the appropriate event notifier so the
controller's poll loop wakes up.
Args:
serializer: Sync serializer implementation.
deserializer: Sync deserializer implementation.
max_workers: Thread pool size. Default 1 (serialization is
typically CPU-bound, more threads may help if the transform
releases the GIL).
"""
def __init__(
self,
serializer: Serializer,
deserializer: Deserializer,
max_workers: int = 1,
) -> None:
self._serializer = serializer
self._deserializer = deserializer
self._serialize_efd = create_event_notifier()
self._deserialize_efd = create_event_notifier()
self._lock = threading.Lock()
self._next_task_id: SerdeTaskId = 0
# task_id -> (success: bool) for completed tasks, partitioned by type
self._completed_serialize: dict[SerdeTaskId, bool] = {}
self._completed_deserialize: dict[SerdeTaskId, bool] = {}
self._pool = ThreadPoolExecutor(max_workers=max_workers)
# ----- Event fds -----
def get_serialize_event_fd(self) -> int:
"""Return the fd signaled on serialize completion."""
return self._serialize_efd.fileno()
def get_deserialize_event_fd(self) -> int:
"""Return the fd signaled on deserialize completion."""
return self._deserialize_efd.fileno()
# ----- Serialize -----
def submit_serialize(
self,
src_objs: list[MemoryObj],
dst_objs: list[MemoryObj],
) -> SerdeTaskId:
"""Submit a batch serialize task to the thread pool."""
task_id = self._alloc_task_id()
logger.debug(
"Serde: submitted serialize task %d (%d objects)",
task_id,
len(src_objs),
)
self._pool.submit(
self._run_task,
task_id,
_TaskType.SERIALIZE,
src_objs,
dst_objs,
)
return task_id
def query_serialize_result(self, task_id: SerdeTaskId) -> bool | None:
"""Pop and return the serialize task result, or None if pending."""
with self._lock:
return self._completed_serialize.pop(task_id, None)
# ----- Deserialize -----
def submit_deserialize(
self,
src_objs: list[MemoryObj],
dst_objs: list[MemoryObj],
) -> SerdeTaskId:
"""Submit a batch deserialize task to the thread pool."""
task_id = self._alloc_task_id()
logger.debug(
"Serde: submitted deserialize task %d (%d objects)",
task_id,
len(src_objs),
)
self._pool.submit(
self._run_task,
task_id,
_TaskType.DESERIALIZE,
src_objs,
dst_objs,
)
return task_id
def query_deserialize_result(self, task_id: SerdeTaskId) -> bool | None:
"""Pop and return the deserialize task result, or None if pending."""
with self._lock:
return self._completed_deserialize.pop(task_id, None)
# ----- Size estimation (delegates to sync serializer) -----
def estimate_serialized_size(self, layout_desc: MemoryLayoutDesc) -> int:
"""Delegate to the sync serializer's estimate (includes margin)."""
return self._serializer.estimate_serialized_size(layout_desc)
# ----- Lifecycle -----
def close(self) -> None:
"""Shut down the thread pool and close event notifiers."""
self._pool.shutdown(wait=True)
self._serialize_efd.close()
self._deserialize_efd.close()
# ----- Internal -----
def _alloc_task_id(self) -> SerdeTaskId:
with self._lock:
task_id = self._next_task_id
self._next_task_id += 1
return task_id
def _run_task(
self,
task_id: SerdeTaskId,
task_type: _TaskType,
src_objs: list[MemoryObj],
dst_objs: list[MemoryObj],
) -> None:
"""Execute a serialize/deserialize task in the thread pool.
On completion (success or failure), stores the result and
signals the event notifier.
"""
success = True
try:
if task_type == _TaskType.SERIALIZE:
for src, dst in zip(src_objs, dst_objs, strict=True):
# ``serialize`` returns the actual number of bytes
# written, which may be smaller than the destination
# buffer (since the destination is sized from
# ``estimate_serialized_size``, an upper bound).
# Narrow the destination's logical size to ``n`` so
# downstream L2 adapters that read the size via
# ``obj.get_size()`` / ``obj.byte_array`` store
# exactly ``n`` bytes -- not the over-allocated
# estimate. Guarded so duck-typed test fakes that
# don't implement the interface still work, and so a
# serializer that does not honor the ``-> int``
# contract (returns ``None``) is skipped rather than
# tripping set_used_size's range check.
n = self._serializer.serialize(src, dst)
if isinstance(n, int) and hasattr(dst, "set_used_size"):
dst.set_used_size(n)
else:
for src, dst in zip(src_objs, dst_objs, strict=True):
self._deserializer.deserialize(src, dst)
except Exception:
logger.exception(
"Serde task %d (%s) failed",
task_id,
task_type.name,
)
success = False
if success:
logger.debug(
"Serde: %s task %d completed successfully (%d objects)",
task_type.name.lower(),
task_id,
len(src_objs),
)
else:
logger.warning(
"Serde: %s task %d failed (%d objects)",
task_type.name.lower(),
task_id,
len(src_objs),
)
with self._lock:
if task_type == _TaskType.SERIALIZE:
self._completed_serialize[task_id] = success
else:
self._completed_deserialize[task_id] = success
# Signal the appropriate notifier to wake the controller's poll loop
notifier = (
self._serialize_efd
if task_type == _TaskType.SERIALIZE
else self._deserialize_efd
)
try:
notifier.notify()
except OSError:
logger.exception("Failed to signal notifier for serde task %d", task_id)