项目文件夹

文件
2026-07-13 12:24:33 +08:00

261 行
9.3 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
from contextlib import nullcontext
from typing import TYPE_CHECKING, Optional
import threading
import time
# First Party
from lmcache.logging import init_logger
from lmcache.observability import LMCStatsMonitor, PrometheusLogger
from lmcache.v1.periodic_thread import (
PeriodicThread,
PeriodicThreadRegistry,
ThreadLevel,
ThreadRunSummary,
)
if TYPE_CHECKING:
# First Party
from lmcache.v1.config import LMCacheEngineConfig
from lmcache.v1.memory_management import MemoryObj
from lmcache.v1.metadata import LMCacheMetadata
logger = init_logger(__name__)
class PinMonitor(PeriodicThread):
"""
Global monitor (singleton per process, shared across all cache engines)
for pinned TensorMemoryObj instances to handle timeout detection.
This class runs a background thread that periodically checks for pinned objects
that have exceeded their timeout duration.
"""
_instance = None
_instance_lock = threading.Lock() # Class-level lock for singleton pattern
def __init__(self, config: "LMCacheEngineConfig") -> None:
# Initialize PeriodicThread base class
super().__init__(
name="PinMonitor-thread",
interval=config.pin_check_interval_sec,
level=ThreadLevel.CRITICAL,
init_wait=0.0,
)
# obj_id is the virtual memory address given by Python's id() function
self._pinned_objects: dict[
int, tuple["MemoryObj", float]
] = {} # {obj_id: (memory_obj, register_time)}
self._objects_lock = threading.Lock()
self._check_interval = config.pin_check_interval_sec
self._pin_timeout_sec = config.pin_timeout_sec
# Register with the global registry
PeriodicThreadRegistry.get_instance().register(self)
# Auto-start the monitor on first instance creation
self.start_monitoring()
@staticmethod
def GetOrCreate(
config: Optional["LMCacheEngineConfig"] = None,
metadata: Optional["LMCacheMetadata"] = None,
) -> "PinMonitor":
"""Get or create the singleton instance.
Args:
config: Required for first-time initialization.
Optional for subsequent calls.
metadata: Metadata for the label view that should publish
PinMonitor metrics.
Raises:
ValueError: If config is None when creating the instance
for the first time.
"""
if PinMonitor._instance is None:
with PinMonitor._instance_lock:
if PinMonitor._instance is None:
assert config is not None, "config is required"
PinMonitor._instance = PinMonitor(config)
instance = PinMonitor._instance
assert instance is not None
if metadata is not None:
assert config is not None, "config is required to set up metrics"
instance._setup_metrics(config, metadata)
return instance
def on_pin(self, memory_obj: "MemoryObj"):
"""Register a pinned memory object for timeout monitoring.
Note: The same memory_obj can be pinned multiple times, so this
function may be called multiple times with the same object.
Each call updates the register time, effectively resetting the
timeout countdown.
"""
obj_id = id(memory_obj)
with self._objects_lock:
current_time = time.time()
self._pinned_objects[obj_id] = (memory_obj, current_time)
logger.debug(
"Registered pinned object %s for timeout monitoring at time %.2f",
obj_id,
current_time,
)
def on_unpin(self, memory_obj: "MemoryObj"):
"""Unregister a memory object from timeout monitoring."""
obj_id = id(memory_obj)
with self._objects_lock:
if obj_id in self._pinned_objects:
del self._pinned_objects[obj_id]
logger.debug(
"Unregistered pinned object %s from timeout monitoring",
obj_id,
)
def _check_timeouts(self) -> tuple[int, int, int]:
"""Check all registered pinned objects for timeout.
Returns:
tuple: (pinned_count, timeout_count, force_unpin_success_count)
"""
current_time = time.time()
timeout_objects = []
with self._objects_lock:
pinned_count = len(self._pinned_objects)
for obj_id, (memory_obj, register_time) in list(
self._pinned_objects.items()
):
# Check if object is still pinned and has exceeded timeout
if memory_obj.meta.pin_count > 0:
elapsed_time = current_time - register_time
if elapsed_time > self._pin_timeout_sec:
timeout_objects.append((memory_obj, elapsed_time))
# Force unpin timeout objects outside the lock to avoid deadlocks
force_unpin_success_count = 0
for memory_obj, elapsed_time in timeout_objects:
try:
self._force_unpin_timeout_object(memory_obj, elapsed_time)
force_unpin_success_count += 1
except Exception as e:
logger.error(
"Error forcing unpin for timeout object %s: %s", id(memory_obj), e
)
if force_unpin_success_count > 0:
logger.warning(
"Force unpinned %d timeout objects in %d pinned_objects "
"within %d seconds",
force_unpin_success_count,
pinned_count,
self._pin_timeout_sec,
)
else:
logger.debug(
"PinMonitor check: pinned_objects=%d, timeout_objects=%d, "
"force_unpin_success=%d",
pinned_count,
len(timeout_objects),
force_unpin_success_count,
)
return pinned_count, len(timeout_objects), force_unpin_success_count
def _force_unpin_timeout_object(self, memory_obj: "MemoryObj", elapsed_time: float):
"""Force unpin a timeout object and log the event."""
# Get current pin_count without holding the lock for unpin calls
# Use nullcontext if memory_obj doesn't have a lock attribute
obj_lock = getattr(memory_obj, "lock", None) or nullcontext()
with obj_lock:
current_pin_count = memory_obj.meta.pin_count
if current_pin_count <= 0:
return
logger.warning(
"Pin timeout detected for MemoryObj %s. "
"Pin count: %s, Elapsed time: %.2fs. Forcing unpin to 0.",
memory_obj.meta.address,
current_pin_count,
elapsed_time,
)
# Update forced unpin statistics
LMCStatsMonitor.GetOrCreate().update_forced_unpin_count(1)
# Call unpin() while pin_count > 0 to properly release resources
while memory_obj.meta.pin_count > 0:
memory_obj.unpin()
def _execute(self) -> ThreadRunSummary:
"""
Execute one pin monitor check cycle.
This method is called by the PeriodicThread base class.
Returns:
ThreadRunSummary: Summary of the check cycle
"""
pinned_count, timeout_count, force_unpin_count = self._check_timeouts()
return ThreadRunSummary(
success=True,
message=f"Checked {pinned_count} objects, {timeout_count} timeouts, "
f"{force_unpin_count} force unpinned",
extra_info={
"pinned_count": str(pinned_count),
"timeout_count": str(timeout_count),
"force_unpin_count": str(force_unpin_count),
},
)
def start_monitoring(self) -> None:
"""Start the background monitoring thread."""
if self.is_running:
return
# Use base class start method
self.start()
logger.info("PinMonitor started")
def _setup_metrics(
self,
config: "LMCacheEngineConfig",
metadata: "LMCacheMetadata",
) -> None:
prometheus_logger = PrometheusLogger.GetOrCreate(metadata, config=config)
prometheus_logger.pin_monitor_pinned_objects_count.set_function(
lambda: len(self._pinned_objects)
)
def stop_monitoring(self):
"""Stop the background monitoring thread."""
if not self.is_running:
return
# Unregister from the global registry
PeriodicThreadRegistry.get_instance().unregister(self.name)
# Use base class stop method
self.stop()
logger.info("PinMonitor stopped")
def get_monitored_count(self) -> int:
"""Get the number of currently monitored pinned objects."""
with self._objects_lock:
return len(self._pinned_objects)
@staticmethod
def DestroyInstance():
"""Destroy the singleton instance and stop monitoring.
This is mainly used for testing to ensure clean state between tests.
"""
with PinMonitor._instance_lock:
if PinMonitor._instance is not None:
PinMonitor._instance.stop_monitoring()
PinMonitor._instance = None