Skip to content

fix(WTinyLFU): match the TinyLFU paper's sketch (window exit, 4-bit counters, sample size) - #2

Open
marcft wants to merge 2 commits into
developfrom
wtinylfu/match-paper
Open

marcft wants to merge 2 commits into
developfrom
wtinylfu/match-paper

Conversation

@marcft

@marcft marcft commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Hey, I saw your note on 1a1a11a#376 about the other spots where
WTinyLFU.c doesn't match the paper, and that fixing just one of them made things
worse. So I tried doing them together.

This PR fixes three of them:

  • leaving the window doesn't add 1 to the count anymore
  • counts stop at 15 (4 bits like Caffeine, the paper would use 10)
  • the sketch gets halved every 10x the cache size instead of 32x, and the
    request counter gets halved too instead of going back to 0

I left a few things out on purpose:

  • there is still no doorkeeper
  • sizes are still in bytes, so with big objects the halving almost never
    happens
  • the CBF still bumps every counter, not only the smallest (checked changing
    that would change Merlin too)

Results are kind of mixed. It helps a lot on the prefix cache traces
(FreeInference goes from 0.85 to 0.93 hit ratio), barely moves anything with the
default SLRU main, and it's worse with LRU as main. All the numbers and commands
are below. If you'd rather not change the defaults I can put it behind a
parameter instead.

Follow-up to 1a1a11a#376. Related: 1a1a11a#21.

Type of change

  • Bug fix (algorithm fidelity; changes results)
  • New eviction / admission / prefetch algorithm
  • New trace reader or trace format
  • Performance improvement
  • Documentation
  • Build / CI
  • Other:

How was it tested?

  • test_WTinyLFU_counts_window_hits is tightened. Object 102 is now read
    twice instead of six times, so it must beat a victim read once. It fails on
    develop (test_evictionAlgo.c:617, 102 is dropped) and passes with commit 1.

  • test_minimalIncrementCBF_max_count is new. With a cap of 15, 20 adds
    give an estimate of 15.

  • Merlin is unchanged. Same miss ratios on develop and here for
    cachesim ../data/cloudPhysicsIO.vscsi vscsi merlin 0.01,0.1 and
    cachesim Zipf_2000000_100000_1.0 syn merlin 0.01,0.1.

  • CLI tests. cd _build && bash ../test/test_cli.sh: 419 passed, 0 failed.

  • Sanitizers. Both tests are clean under ASan and UBSan. LeakSanitizer
    isn't available on macOS; CI's Linux job runs it.

  • ctest --test-dir _build --output-on-failure passes (15/15, Release, macOS)

  • Build is warning-free with the strict flags from code-quality.yml. The
    only error locally is the existing bin/debug/main.cpp:27 -Wmain from
    Apple clang, which develop has too.

Results

LLM prefix-cache traces (block hit ratio, higher is better)

The traces are FreeInference and Chutes from
harvardMadsys/2026_prefixcache
(rev ba89a34). The simulator is the prefixsim driver from
LauYeeYu/libCacheSim@0006aa5, the one cacheMon/PrefixSim ships, built
against develop and against this PR.

prefixsim --trace <trace>.lcp.zst --cache-size <blocks> --algo <wtinylfu|lru> --cost-model qwen3coder30b_blksz_16
trace cache (blocks) develop this PR LRU
FreeInference 65,536 (96 GiB) 0.8709 0.9058 0.9175
FreeInference 699,051 (1 TiB) 0.8485 0.9259 0.9376
Chutes 65,536 (96 GiB) 0.6334 0.6616 0.7269
Chutes 699,051 (1 TiB) 0.6172 0.6820 0.7587

Every row improves, and the FreeInference curve no longer dips from 96 GiB to
1 TiB. As in 1a1a11a#376, the window-exit fix alone is a bit worse; see "Which change
does what" below.

All runs have 0 unexpected evictions. prefixsim does warn that W-TinyLFU can
evict a block the arriving request has matched, which an engine that pins a
running request's blocks would not do. On FreeInference at 1 TiB,
n_self_eviction is 5,394,352 on develop and 1,281,730 with this PR.

Classic traces (miss ratio, lower is better)

These are 1a1a11a#376's traces and sizes. As in 1a1a11a#376, the 0.1% Zipf size is left out:
it misses 100% in every version, because the 1% window rounds down to zero
objects.

# from _build; <main> is SLRU or LRU
./bin/cachesim ../data/cloudPhysicsIO.oracleGeneral.bin oracleGeneral wtinyLFU 0.001,0.01,0.05,0.1 -e main-cache=<main>
./bin/cachesim ../data/twitter_cluster52.csv csv -t "time-col=1,obj-id-col=2,obj-size-col=3,has-header=true,obj-id-is-num=1" wtinyLFU 0.001,0.01,0.05,0.1 -e main-cache=<main>
./bin/cachesim Zipf_2000000_100000_0.8 syn wtinyLFU 0.001,0.01,0.05,0.1 -e main-cache=<main>
./bin/cachesim Zipf_2000000_100000_1.0 syn wtinyLFU 0.001,0.01,0.05,0.1 -e main-cache=<main>

Default main cache (SLRU): every difference is within −0.0031 / +0.0062.

trace sizes develop → this PR
cloudPhysicsIO 1 / 19 / 96 / 193 MiB 0.8346→0.8365, 0.8258→0.8227, 0.8040→0.8029, 0.7663→0.7663
twitter_cluster52 28 / 285 / 1427 / 2854 KiB 0.5287→0.5331, 0.3321→0.3383, 0.2182→0.2186, 0.1871→0.1881
Zipf α=0.8 997 / 4988 / 9977 objs 0.6862→0.6878, 0.5299→0.5313, 0.4448→0.4461
Zipf α=1.0 940 / 4700 / 9400 objs 0.4074→0.4092, 0.2757→0.2772, 0.2189→0.2207

main-cache=LRU: never better, and up to +0.0354 worse.

trace sizes develop → this PR
cloudPhysicsIO 1 / 19 / 96 / 193 MiB 0.8386→0.8387, 0.8292→0.8314, 0.7956→0.8065, 0.7774→0.7774
twitter_cluster52 28 / 285 / 1427 / 2854 KiB 0.5322→0.5510, 0.3492→0.3704, 0.2376→0.2683, 0.2085→0.2214
Zipf α=0.8 997 / 4988 / 9977 objs 0.6935→0.7289, 0.5279→0.5572, 0.4418→0.4636
Zipf α=1.0 940 / 4700 / 9400 objs 0.4091→0.4388, 0.2749→0.2997, 0.2182→0.2401

Which change does what

"Cap only" and "10x only" are commit 1 plus one half of commit 2. Prefix rows are
at 1 TiB, as hit ratio; classic rows are miss ratio.

develop + commit 1 + cap only + 10x only this PR
FreeInference (hit) 0.8485 0.8287 0.8116 0.9158 0.9259
Chutes (hit) 0.6172 0.6050 0.5337 0.6703 0.6820
twitter, LRU, 1427 KiB (miss) 0.2376 0.2460 0.2683 0.2460 0.2683
twitter, SLRU, 285 KiB (miss) 0.3321 0.3317 0.3383 0.3317 0.3383
Zipf α=0.8, LRU, 997 objs (miss) 0.6935 0.6888 0.7426 0.7291 0.7289
cloudPhysicsIO, LRU, 96 MiB (miss) 0.7956 0.8065 0.8065 0.8065 0.8065

Reading these:

  • The 10x period is what helps on the prefix-cache traces (+8.7 / +6.5
    points over commit 1). There, sessions start and finish, so the popular set
    keeps moving. It is also what costs on stationary Zipf with an LRU main cache,
    where a longer sample estimates popularity better.
  • The cap only helps together with the shorter period. On its own it lowers
    the prefix hit ratio (−1.7 / −7.1 points); on top of 10x it adds about 1
    point. On twitter it is what raises miss ratios, by up to +2.2 points with an
    LRU main.
  • On the byte-sized traces (cloudPhysicsIO, and twitter at 285 KiB and
    above), the period never elapses within the trace, so commit 2's halving
    changes nothing there.

A follow-up could count the period in objects, so it also elapses on traces
with large objects.

All sizes and variants (miss ratio)
main trace size develop + commit 1 + cap only + 10x only this PR
SLRU cloudPhysicsIO 1 MiB 0.8346 0.8365 0.8365 0.8365 0.8365
SLRU cloudPhysicsIO 19 MiB 0.8258 0.8227 0.8227 0.8227 0.8227
SLRU cloudPhysicsIO 96 MiB 0.8040 0.8029 0.8029 0.8029 0.8029
SLRU cloudPhysicsIO 193 MiB 0.7663 0.7663 0.7663 0.7663 0.7663
SLRU twitter_cluster52 28 KiB 0.5287 0.5288 0.5346 0.5281 0.5331
SLRU twitter_cluster52 285 KiB 0.3321 0.3317 0.3383 0.3317 0.3383
SLRU twitter_cluster52 1427 KiB 0.2182 0.2186 0.2186 0.2186 0.2186
SLRU twitter_cluster52 2854 KiB 0.1871 0.1880 0.1881 0.1880 0.1881
SLRU Zipf α=0.8 997 objs 0.6862 0.6858 0.6877 0.6876 0.6878
SLRU Zipf α=0.8 4988 objs 0.5299 0.5289 0.5311 0.5312 0.5313
SLRU Zipf α=0.8 9977 objs 0.4448 0.4433 0.4447 0.4462 0.4461
SLRU Zipf α=1.0 940 objs 0.4074 0.4065 0.4082 0.4092 0.4092
SLRU Zipf α=1.0 4700 objs 0.2757 0.2748 0.2750 0.2772 0.2772
SLRU Zipf α=1.0 9400 objs 0.2189 0.2183 0.2183 0.2207 0.2207
LRU cloudPhysicsIO 1 MiB 0.8386 0.8387 0.8387 0.8387 0.8387
LRU cloudPhysicsIO 19 MiB 0.8292 0.8314 0.8314 0.8314 0.8314
LRU cloudPhysicsIO 96 MiB 0.7956 0.8065 0.8065 0.8065 0.8065
LRU cloudPhysicsIO 193 MiB 0.7774 0.7774 0.7774 0.7774 0.7774
LRU twitter_cluster52 28 KiB 0.5322 0.5336 0.5536 0.5321 0.5510
LRU twitter_cluster52 285 KiB 0.3492 0.3496 0.3704 0.3496 0.3704
LRU twitter_cluster52 1427 KiB 0.2376 0.2460 0.2683 0.2460 0.2683
LRU twitter_cluster52 2854 KiB 0.2085 0.2128 0.2214 0.2128 0.2214
LRU Zipf α=0.8 997 objs 0.6935 0.6888 0.7426 0.7291 0.7289
LRU Zipf α=0.8 4988 objs 0.5279 0.5245 0.5608 0.5575 0.5572
LRU Zipf α=0.8 9977 objs 0.4418 0.4386 0.4564 0.4636 0.4636
LRU Zipf α=1.0 940 objs 0.4091 0.4050 0.4300 0.4388 0.4388
LRU Zipf α=1.0 4700 objs 0.2749 0.2720 0.2761 0.2997 0.2997
LRU Zipf α=1.0 9400 objs 0.2182 0.2163 0.2169 0.2401 0.2401

Checklist

  • Formatted with clang-format
  • Added or updated tests
  • Added or updated documentation (the file's header lists what still
    differs from the paper)
  • New algorithms are registered in the CLI and listed in the README: n/a

Copilot AI balanced review requested due to automatic review settings November 7, 2026 19:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

WTinyLFU_evict added 1 to the frequency sketch for every object as it left
the window, whether it was admitted to the main cache or dropped. Leaving the
window is not an access, so an object read once was counted twice, and a
main-cache victim read once tied a candidate read twice; a tie keeps the
victim. The W-TinyLFU paper, Caffeine and mandreyel/w-tinylfu, which this file
credits, do not count it. Remove the increment.

test_WTinyLFU_counts_window_hits now reads object 102 twice instead of six
times, so it must beat a victim read once: it needs both the window hit
counted and the window exit not counted, and fails without this change.

Follow-up to 1a1a11a#376, which lists this difference.
@marcft
marcft force-pushed the wtinylfu/match-paper branch 3 times, most recently from f2b1449 to 430a5cf Compare November 7, 2026 22:23
The sketch's counters were unbounded, and the sketch was halved after
32 x the main cache's size accesses, with the access count restarting
at 0. Stop counters at 15 (4 bits) and halve after 10 x the cache size
accesses, as Caffeine does; this is the Caffeine configuration that the
TinyLFU paper evaluates. Halve the access count along with the
counters, as the paper's reset does (Sec. 3.3), so later halvings come
every 5 x the cache size accesses.

minimalIncrementCBF gains a max_count field. It defaults to UINT_MAX,
so Merlin is unchanged.

The file's preamble now lists what still differs from the paper: there
is no doorkeeper, the sketch is sized and its period counted in bytes,
an add increments all of an object's counters, and the cap is 15 rather
than the paper's W/C.

Follow-up to 1a1a11a#376, which lists these differences.
@marcft
marcft force-pushed the wtinylfu/match-paper branch from 430a5cf to 0e3eb1c Compare November 7, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants