main
py 223 lines 6.37 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from collections import UserDict
4 from collections.abc import Callable, Hashable, Iterator, KeysView, Mapping
5 from types import MappingProxyType
6 from typing import NamedTuple, TypeVar, cast, overload
7
8 import cachetools
9
10 _K = TypeVar("_K", bound=Hashable)
11 _V = TypeVar("_V")
12 _T = TypeVar("_T")
13
14
15 class _Sentinel: ...
16
17
18 ALL_PINNED_SENTINEL = _Sentinel()
19
20
21 class _MappingOrderCacheView(UserDict[_K, _V]):
22 def __init__(self, data: Mapping[_K, _V], ordered_keys: Mapping[_K, None]):
23 super().__init__(data)
24 self.ordered_keys = ordered_keys
25
26 def __iter__(self) -> Iterator[_K]:
27 return iter(self.ordered_keys)
28
29 def keys(self) -> KeysView[_K]:
30 return KeysView(self.ordered_keys)
31
32
33 class CacheInfo(NamedTuple):
34 hits: int
35 total: int
36
37 @property
38 def hit_ratio(self) -> float:
39 if self.total == 0:
40 return 0
41
42 return self.hits / self.total
43
44 def __sub__(self, other: "CacheInfo"):
45 return CacheInfo(
46 hits=self.hits - other.hits,
47 total=self.total - other.total,
48 )
49
50
51 class LRUCache(cachetools.LRUCache[_K, _V]):
52 def __init__(self, capacity: float, getsizeof: Callable[[_V], float] | None = None):
53 super().__init__(capacity, getsizeof)
54
55 self.pinned_items = set[_K]()
56
57 self._hits = 0
58 self._total = 0
59 self._last_info = CacheInfo(hits=0, total=0)
60
61 def __getitem__(self, key: _K, *, update_info: bool = True) -> _V:
62 value = super().__getitem__(key)
63
64 if update_info:
65 self._hits += 1
66 self._total += 1
67
68 return value
69
70 def __delitem__(self, key: _K) -> None:
71 run_on_remove = key in self
72 value = self.__getitem__(key, update_info=False) # type: ignore[call-arg]
73 super().__delitem__(key)
74 if key in self.pinned_items:
75 # Todo: add warning to inform that del pinned item
76 self._unpin(key)
77 if run_on_remove:
78 self._on_remove(key, value)
79
80 @property
81 def cache(self) -> Mapping[_K, _V]:
82 """Return the internal cache dictionary in order (read-only)."""
83 return _MappingOrderCacheView(
84 self._Cache__data, # type: ignore
85 self.order,
86 )
87
88 @property
89 def order(self) -> Mapping[_K, None]:
90 """Return the internal order dictionary (read-only)."""
91 return MappingProxyType(self._LRUCache__order) # type: ignore
92
93 @property
94 def capacity(self) -> float:
95 return self.maxsize
96
97 @property
98 def usage(self) -> float:
99 if self.maxsize == 0:
100 return 0
101
102 return self.currsize / self.maxsize
103
104 def stat(self, *, delta: bool = False) -> CacheInfo:
105 """Gets the cumulative number of hits and queries against this cache.
106
107 If `delta=True`, instead gets these statistics
108 since the last call that also passed `delta=True`.
109 """
110 info = CacheInfo(hits=self._hits, total=self._total)
111
112 if delta:
113 info_delta = info - self._last_info
114 self._last_info = info
115 info = info_delta
116
117 return info
118
119 def touch(self, key: _K) -> None:
120 if key in self:
121 self._LRUCache__order.move_to_end(key) # type: ignore
122
123 @overload
124 def get(self, key: _K, /) -> _V | None: ...
125
126 @overload
127 def get(self, key: _K, /, default: _V | _T) -> _V | _T: ...
128
129 def get(self, key: _K, /, default: _V | _T | None = None) -> _V | _T | None:
130 value: _V | _T | None
131 if key in self:
132 value = self.__getitem__(key, update_info=False) # type: ignore[call-arg]
133
134 self._hits += 1
135 else:
136 value = default
137
138 self._total += 1
139 return value
140
141 @overload
142 def pop(self, key: _K) -> _V: ...
143
144 @overload
145 def pop(self, key: _K, default: _V | _T) -> _V | _T: ...
146
147 def pop(self, key: _K, default: _V | _T | None = None) -> _V | _T | None:
148 value: _V | _T | None
149 if key not in self:
150 return default
151
152 value = self.__getitem__(key, update_info=False) # type: ignore[call-arg]
153 self.__delitem__(key)
154 return value
155
156 def put(self, key: _K, value: _V) -> None:
157 self.__setitem__(key, value)
158
159 def put_if_fits(self, key: _K, value: _V) -> bool:
160 """Insert `value` if it is not larger than the cache capacity.
161
162 Unlike `put`, this does not raise when a single item exceeds
163 `maxsize`. Size is computed once inside the insert path.
164
165 Returns:
166 `True` if the item was cached, otherwise `False`.
167
168 """
169 try:
170 self[key] = value
171 except ValueError as e:
172 if str(e) != "value too large":
173 raise
174 return False
175 return True
176
177 def pin(self, key: _K) -> None:
178 """Pins a key in the cache preventing it from being
179 evicted in the LRU order.
180 """
181 if key not in self:
182 raise ValueError(f"Cannot pin key: {key} not in cache.")
183 self.pinned_items.add(key)
184
185 def _unpin(self, key: _K) -> None:
186 """Unpins a key in the cache allowing it to be
187 evicted in the LRU order.
188 """
189 self.pinned_items.remove(key)
190
191 def _on_remove(self, key: _K, value: _V | None) -> None:
192 pass
193
194 def remove_oldest(self, *, remove_pinned: bool = False) -> None:
195 if len(self) == 0:
196 return
197
198 self.popitem(remove_pinned=remove_pinned)
199
200 def popitem(self, remove_pinned: bool = False):
201 """Remove and return the `(key, value)` pair least recently used."""
202 if not remove_pinned:
203 # pop the oldest item in the cache that is not pinned
204 lru_key = next(
205 (key for key in self.order if key not in self.pinned_items),
206 ALL_PINNED_SENTINEL,
207 )
208 if lru_key is ALL_PINNED_SENTINEL:
209 raise RuntimeError(
210 "All items are pinned, cannot remove oldest from the cache."
211 )
212 else:
213 lru_key = next(iter(self.order))
214 value = self.pop(cast(_K, lru_key))
215 return (lru_key, value)
216
217 def clear(self) -> None:
218 while len(self) > 0:
219 self.remove_oldest(remove_pinned=True)
220
221 self._hits = 0
222 self._total = 0
223 self._last_info = CacheInfo(hits=0, total=0)