Skip to content

Commit 62c3a20

Browse files
Haochengclaude
authored andcommitted
[Feat] Add MQ, LRU-K and Clock2QPlus bindings
Bump the libCacheSim submodule to the latest develop (dbf8423..373aee9), which brings three new eviction algorithms, and expose them in Python. New algorithms: - LRUK: evicts by largest backward K-distance (k) - MQ: multi-queue hierarchy with a Qout ghost queue (n_queue, lifetime, qout_size_ratio) - Clock2QPlus: 2Q variant over a Clock main cache (fifo_size_ratio, ghost_size_ratio, move_to_main_threshold, corr_window_ratio) Each is wired through export_cache.cpp, cache.py, __init__.py and the type stubs, covered by the shared parametrized tests, and documented in both the English and Chinese algorithm reference. Two fixes to existing bindings found while checking the docs against the implementation: - BeladySize was unusable: it emitted the key "n-samples" while the C parser only accepts "n-sample", so every construction hit ERROR() and aborted the interpreter. It was absent from every test parametrize list, which is why this went unnoticed. Added regression tests for BeladySize and Belady. - WTinyLFU's window_size was described as a fraction of the main cache. WTinyLFU.c scales the total cache size by it and gives the main cache the remainder, so the docstring and both translations now say total. Also completed the CacheBase type stub, which was missing its admissioner parameter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a60050e commit 62c3a20

8 files changed

Lines changed: 271 additions & 8 deletions

File tree

‎docs/src/en/examples/simulation.md‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ The following cache classes all inherit from `CacheBase` and share a common inte
128128

129129
- *No additional parameters beyond the common arguments*
130130

131+
### LRUK
132+
**LRU-K** evicts the object with the largest backward K-distance, that is the one whose K-th most recent access is oldest. Objects seen fewer than K times have an infinite backward K-distance and are evicted first in FIFO order, so a single access is not enough to earn a place in the cache.
133+
134+
- `k: int` - Number of recent accesses tracked per object (default: `2`)
135+
131136
### FIFO
132137
**First-In, First-Out** evicts objects in order regardless of frequency or recency.
133138

@@ -182,11 +187,18 @@ The following cache classes all inherit from `CacheBase` and share a common inte
182187

183188
- *No additional parameters beyond the common arguments*
184189

190+
### MQ
191+
**Multi-Queue** spreads objects over a hierarchy of LRU queues ordered by access frequency, promoting an object a queue at a time as it is reused and demoting it again if it goes untouched for its lifetime. Eviction always takes the tail of the lowest non-empty queue, and a FIFO ghost queue (`Qout`) remembers the frequency of evicted objects so a quick return restores an object to its former queue.
192+
193+
- `n_queue: int` - Number of queues in the hierarchy, must be in `[1, 64]` (default: `8`)
194+
- `lifetime: int` - Requests an object may go unaccessed before it is demoted a queue (default: `10000`)
195+
- `qout_size_ratio: float` - Size of the `Qout` ghost queue as a multiple of the cache size, must be in `(0, 64]` (default: `4.0`)
196+
185197
### WTinyLFU
186198
**Window TinyLFU** places a small LRU window in front of a larger main cache, and uses a frequency sketch to decide whether an object leaving the window deserves to displace the main cache's eviction candidate.
187199

188200
- `main_cache: str` - Eviction algorithm used for the main cache (default: `"SLRU"`)
189-
- `window_size: float` - Size of the LRU window as a fraction of the main cache (default: `0.01`)
201+
- `window_size: float` - Size of the LRU window as a fraction of the **total** cache size, must be in `[0, 1)`; the main cache receives the remainder (default: `0.01`)
190202

191203
### LeCaR
192204
**Learning Cache Replacement** maintains both an LRU and an LFU candidate and picks between them using weights updated by regret minimisation, so it adapts as the workload shifts between recency- and frequency-friendly.
@@ -205,6 +217,14 @@ The following cache classes all inherit from `CacheBase` and share a common inte
205217
- `init_ref: int` - Initial reference count given to newly admitted objects (default: `0`)
206218
- `init_ratio_cold: float` - Initial fraction of the cache designated as cold (default: `0.5`)
207219

220+
### Clock2QPlus
221+
**Clock-2Q+** is a 2Q variant that puts a small FIFO probationary queue in front of a Clock main cache. A ghost queue tracks recently evicted identifiers, and an adaptive correlation window tunes how long an object must survive the FIFO queue before it is considered worth promoting.
222+
223+
- `fifo_size_ratio: float` - Size of the FIFO queue as a fraction of the cache (default: `0.1`)
224+
- `ghost_size_ratio: float` - Size of the ghost queue as a fraction of the cache (default: `0.9`)
225+
- `move_to_main_threshold: int` - Number of hits before an object is promoted to the main cache (default: `1`)
226+
- `corr_window_ratio: float` - Initial size of the correlation window as a fraction of the FIFO queue (default: `0.5`)
227+
208228
### Cacheus
209229
**Cacheus** builds on `LeCaR`, adding lightweight adaptation of the learning rate and scan/churn detection so that it degrades gracefully on the workloads where `LeCaR` struggles.
210230

‎docs/src/zh/examples/simulation.md‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ for req in reader:
121121

122122
- *除公共参数外无额外参数*
123123

124+
### LRUK
125+
**LRU-K** 淘汰后向 K 距离(backward K-distance)最大的对象,也就是第 K 次最近访问时间最早的那个。访问次数不足 K 次的对象后向 K 距离视为无穷大,会按 FIFO 顺序被优先淘汰,因此仅被访问一次的对象不足以在缓存中站稳脚跟。
126+
127+
- `k: int` —— 每个对象跟踪的最近访问次数(默认:`2`)
128+
124129
### FIFO
125130
**First-In, First-Out(先进先出)** 按进入顺序淘汰对象,不考虑访问频率和时间局部性。
126131

@@ -175,11 +180,18 @@ for req in reader:
175180

176181
- *除公共参数外无额外参数*
177182

183+
### MQ
184+
**Multi-Queue(多队列)** 把对象分布在一组按访问频率排序的 LRU 队列中:对象被重复访问时逐级晋升,在其生命期(lifetime)内未被访问则逐级降级。淘汰总是从最低的非空队列尾部取出,同时用一个 FIFO 幽灵队列(`Qout`)记录被淘汰对象的频率,使得很快再次被访问的对象能够回到原来的队列。
185+
186+
- `n_queue: int` —— 队列层数,取值需在 `[1, 64]` 内(默认:`8`)
187+
- `lifetime: int` —— 对象未被访问多少个请求后降级一层(默认:`10000`)
188+
- `qout_size_ratio: float` —— `Qout` 幽灵队列大小相对缓存大小的倍数,取值需在 `(0, 64]` 内(默认:`4.0`)
189+
178190
### WTinyLFU
179191
**Window TinyLFU** 在一个较大的主缓存前面放置一个小的 LRU 窗口,并用频率草图(frequency sketch)来判断离开窗口的对象是否值得挤掉主缓存中的淘汰候选者。
180192

181193
- `main_cache: str` —— 主缓存所用的淘汰算法(默认:`"SLRU"`)
182-
- `window_size: float` —— LRU 窗口大小占主缓存的比例(默认:`0.01`)
194+
- `window_size: float` —— LRU 窗口大小占**总缓存大小**的比例,取值需在 `[0, 1)` 内,主缓存获得剩余部分(默认:`0.01`)
183195

184196
### LeCaR
185197
**Learning Cache Replacement** 同时维护一个 LRU 候选者和一个 LFU 候选者,并用基于后悔最小化(regret minimisation)更新的权重在二者之间做选择,从而能随着负载在偏时间局部性与偏频率之间切换而自适应调整。
@@ -198,6 +210,14 @@ for req in reader:
198210
- `init_ref: int` —— 新准入对象的初始引用计数(默认:`0`)
199211
- `init_ratio_cold: float` —— 缓存中初始被划为冷页的比例(默认:`0.5`)
200212

213+
### Clock2QPlus
214+
**Clock-2Q+** 是 2Q 的变体,在 Clock 主缓存前面放置一个小的 FIFO 试用队列。幽灵队列记录最近被淘汰的对象标识,自适应的关联窗口(correlation window)则用于调节对象需要在 FIFO 队列中存活多久才值得晋升。
215+
216+
- `fifo_size_ratio: float` —— FIFO 队列大小占缓存的比例(默认:`0.1`)
217+
- `ghost_size_ratio: float` —— 幽灵队列大小占缓存的比例(默认:`0.9`)
218+
- `move_to_main_threshold: int` —— 对象晋升到主缓存前所需的命中次数(默认:`1`)
219+
- `corr_window_ratio: float` —— 关联窗口初始大小占 FIFO 队列的比例(默认:`0.5`)
220+
201221
### Cacheus
202222
**Cacheus** 在 `LeCaR` 的基础上增加了对学习率的轻量自适应以及扫描/抖动检测,使其在 `LeCaR` 表现不佳的负载上退化得更平缓。
203223

‎libcachesim/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@
2626
ARC,
2727
Clock,
2828
Random,
29+
LRUK,
2930
# Advanced algorithms
3031
S3FIFO,
3132
Sieve,
3233
LIRS,
3334
TwoQ,
3435
SLRU,
36+
MQ,
3537
WTinyLFU,
3638
LeCaR,
3739
LFUDA,
3840
ClockPro,
41+
Clock2QPlus,
3942
Cacheus,
4043
# Optimal algorithms
4144
Belady,
@@ -92,16 +95,19 @@
9295
"ARC",
9396
"Clock",
9497
"Random",
98+
"LRUK",
9599
# Advanced cache algorithms
96100
"S3FIFO",
97101
"Sieve",
98102
"LIRS",
99103
"TwoQ",
100104
"SLRU",
105+
"MQ",
101106
"WTinyLFU",
102107
"LeCaR",
103108
"LFUDA",
104109
"ClockPro",
110+
"Clock2QPlus",
105111
"Cacheus",
106112
# Optimal algorithms
107113
"Belady",

‎libcachesim/__init__.pyi‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class Cache:
140140

141141
class CacheBase:
142142
"""Base class for all cache implementations"""
143-
def __init__(self, _cache: Cache): ...
143+
def __init__(self, _cache: Cache, admissioner: Optional["AdmissionerBase"] = None): ...
144144
def get(self, req: Request) -> bool: ...
145145
def find(self, req: Request, update_cache: bool = True) -> CacheObject: ...
146146
def can_insert(self, req: Request) -> bool: ...
@@ -195,6 +195,11 @@ class Random(CacheBase):
195195
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None
196196
): ...
197197

198+
class LRUK(CacheBase):
199+
def __init__(
200+
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, k: int = 2, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None
201+
): ...
202+
198203
# Advanced algorithms
199204
class S3FIFO(CacheBase):
200205
def __init__(
@@ -221,6 +226,11 @@ class SLRU(CacheBase):
221226
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None
222227
): ...
223228

229+
class MQ(CacheBase):
230+
def __init__(
231+
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, n_queue: int = 8, lifetime: int = 10000, qout_size_ratio: float = 4.0, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None
232+
): ...
233+
224234
class WTinyLFU(CacheBase):
225235
def __init__(
226236
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, main_cache: str = "SLRU", window_size: float = 0.01, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None
@@ -241,6 +251,11 @@ class ClockPro(CacheBase):
241251
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, init_ref: int = 0, init_ratio_cold: float = 0.5, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None
242252
): ...
243253

254+
class Clock2QPlus(CacheBase):
255+
def __init__(
256+
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, fifo_size_ratio: float = 0.1, ghost_size_ratio: float = 0.9, move_to_main_threshold: int = 1, corr_window_ratio: float = 0.5, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None
257+
): ...
258+
244259
class Cacheus(CacheBase):
245260
def __init__(
246261
self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None

‎libcachesim/cache.py‎

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
LIRS_init,
1818
TwoQ_init,
1919
SLRU_init,
20+
LRU_K_init,
21+
MQ_init,
2022
# Advanced algorithms
2123
S3FIFO_init,
2224
Sieve_init,
2325
WTinyLFU_init,
2426
LeCaR_init,
2527
LFUDA_init,
2628
ClockPro_init,
29+
Clock2QPlus_init,
2730
Cacheus_init,
2831
# Optimal algorithms
2932
Belady_init,
@@ -231,6 +234,36 @@ def __init__(
231234
)
232235

233236

237+
class LRUK(CacheBase):
238+
"""LRU-K: evict the object with the largest backward K-distance
239+
240+
Objects accessed fewer than K times have an infinite backward K-distance
241+
and are evicted first in FIFO order.
242+
243+
Special parameters:
244+
k: the number of recent accesses to track (default: 2)
245+
"""
246+
247+
def __init__(
248+
self,
249+
cache_size: int | float,
250+
default_ttl: int = 86400 * 300,
251+
hashpower: int = 24,
252+
consider_obj_metadata: bool = False,
253+
k: int = 2,
254+
admissioner: AdmissionerBase = None,
255+
reader: ReaderProtocol = None,
256+
):
257+
cache_specific_params = f"k={k}"
258+
super().__init__(
259+
_cache=LRU_K_init(
260+
_create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader),
261+
cache_specific_params,
262+
),
263+
admissioner=admissioner
264+
)
265+
266+
234267
class FIFO(CacheBase):
235268
"""First In First Out cache (no special parameters)"""
236269

@@ -447,12 +480,51 @@ def __init__(
447480
)
448481

449482

483+
class MQ(CacheBase):
484+
"""Multi-Queue replacement algorithm
485+
486+
Objects are held in a hierarchy of LRU queues ordered by access frequency;
487+
an object that is not re-accessed within its lifetime is demoted to the
488+
next lower queue. Evicted objects keep their frequency in a FIFO ghost
489+
queue (Qout).
490+
491+
Special parameters:
492+
n_queue: number of queues in the hierarchy, must be in [1, 64] (default: 8)
493+
lifetime: number of requests an object may stay in its queue without being
494+
accessed before it is demoted (default: 10000)
495+
qout_size_ratio: size of the Qout ghost queue relative to the cache size,
496+
must be in (0, 64] (default: 4.0)
497+
"""
498+
499+
def __init__(
500+
self,
501+
cache_size: int | float,
502+
default_ttl: int = 86400 * 300,
503+
hashpower: int = 24,
504+
consider_obj_metadata: bool = False,
505+
n_queue: int = 8,
506+
lifetime: int = 10000,
507+
qout_size_ratio: float = 4.0,
508+
admissioner: AdmissionerBase = None,
509+
reader: ReaderProtocol = None,
510+
):
511+
cache_specific_params = f"n-queue={n_queue}, lifetime={lifetime}, Qout-size-ratio={qout_size_ratio}"
512+
super().__init__(
513+
_cache=MQ_init(
514+
_create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader),
515+
cache_specific_params,
516+
),
517+
admissioner=admissioner
518+
)
519+
520+
450521
class WTinyLFU(CacheBase):
451522
"""Window TinyLFU
452523
453524
Special parameters:
454525
main_cache: the type of the main cache (default: "SLRU")
455-
window_size: ratio of the window size to the main cache size (default: 0.01)
526+
window_size: ratio of the window LRU size to the total cache size, must be
527+
in [0, 1); the main cache receives the remainder (default: 0.01)
456528
"""
457529

458530
def __init__(
@@ -552,6 +624,45 @@ def __init__(
552624
)
553625

554626

627+
class Clock2QPlus(CacheBase):
628+
"""Clock-2Q+ replacement algorithm
629+
630+
A 2Q variant that uses a small FIFO probationary queue in front of a Clock
631+
main cache, with a ghost queue and an adaptive correlation window.
632+
633+
Special parameters:
634+
fifo_size_ratio: ratio of the FIFO queue size to the total cache size (default: 0.1)
635+
ghost_size_ratio: ratio of the ghost queue size to the total cache size (default: 0.9)
636+
move_to_main_threshold: number of hits before an object is promoted to the main cache (default: 1)
637+
corr_window_ratio: initial ratio of the correlation window to the FIFO queue size (default: 0.5)
638+
"""
639+
640+
def __init__(
641+
self,
642+
cache_size: int | float,
643+
default_ttl: int = 86400 * 300,
644+
hashpower: int = 24,
645+
consider_obj_metadata: bool = False,
646+
fifo_size_ratio: float = 0.1,
647+
ghost_size_ratio: float = 0.9,
648+
move_to_main_threshold: int = 1,
649+
corr_window_ratio: float = 0.5,
650+
admissioner: AdmissionerBase = None,
651+
reader: ReaderProtocol = None,
652+
):
653+
cache_specific_params = (
654+
f"fifo-size-ratio={fifo_size_ratio}, ghost-size-ratio={ghost_size_ratio}, "
655+
f"move-to-main-threshold={move_to_main_threshold}, corr-window-ratio={corr_window_ratio}"
656+
)
657+
super().__init__(
658+
_cache=Clock2QPlus_init(
659+
_create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader),
660+
cache_specific_params,
661+
),
662+
admissioner=admissioner
663+
)
664+
665+
555666
class Cacheus(CacheBase):
556667
"""Cacheus algorithm (no special parameters)"""
557668

@@ -606,7 +717,8 @@ def __init__(
606717
admissioner: AdmissionerBase = None,
607718
reader: ReaderProtocol = None,
608719
):
609-
cache_specific_params = f"n-samples={n_samples}"
720+
# NOTE: the C parser accepts "n-sample" (singular)
721+
cache_specific_params = f"n-sample={n_samples}"
610722
super().__init__(
611723
_cache=BeladySize_init(
612724
_create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader),

‎src/export_cache.cpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ void export_cache(py::module& m) {
482482
make_cache_wrapper<CAR_init>("CAR_init")(m);
483483
make_cache_wrapper<Cacheus_init>("Cacheus_init")(m);
484484
make_cache_wrapper<Clock_init>("Clock_init")(m);
485+
make_cache_wrapper<Clock2QPlus_init>("Clock2QPlus_init")(m);
485486
make_cache_wrapper<ClockPro_init>("ClockPro_init")(m);
486487
make_cache_wrapper<FIFO_init>("FIFO_init")(m);
487488
make_cache_wrapper<FIFO_Merge_init>("FIFO_Merge_init")(m);
@@ -495,7 +496,9 @@ void export_cache(py::module& m) {
495496
make_cache_wrapper<LFUDA_init>("LFUDA_init")(m);
496497
make_cache_wrapper<LIRS_init>("LIRS_init")(m);
497498
make_cache_wrapper<LRU_init>("LRU_init")(m);
499+
make_cache_wrapper<LRU_K_init>("LRU_K_init")(m);
498500
make_cache_wrapper<LRU_Prob_init>("LRU_Prob_init")(m);
501+
make_cache_wrapper<MQ_init>("MQ_init")(m);
499502
make_cache_wrapper<nop_init>("nop_init")(m);
500503

501504
make_cache_wrapper<QDLP_init>("QDLP_init")(m);

‎src/libCacheSim‎

Submodule libCacheSim updated 63 files

0 commit comments

Comments
 (0)