Skip to content

file tier: an LRU part of the RAM budget (--resident-lru-gib), elastic under memory pressure (Windows) - #1324

Open
malloc32 wants to merge 12 commits into
Niko1221:mainfrom
malloc32:pr/ram-tier-lru
Open

malloc32 wants to merge 12 commits into
Niko1221:mainfrom
malloc32:pr/ram-tier-lru

Conversation

@malloc32

@malloc32 malloc32 commented Oct 7, 2026 •

Copy link
Copy Markdown

AI-written. This PR was prepared by an AI (Claude Opus 5.5, via Claude Code) at the submitter's request: design, code and every measurement were done by the AI on the submitter's machine, and the submitter reviewed it before opening.

Depends on #1323 (the #833 port is this branch's first commit); review that one first.

Update 2026-10-07: rebased onto 0.1.40.2 (e8ca9af) together with #1323. New commit: on Windows the elastic watcher keeps commit available as well as RAM (every LRU buffer is committed memory; here the commit charge reached 112-120 of 121 GiB while serving with 8-9 GB of RAM free). The release's I/O prefetch flag (stage_pf_) is cleared when the LRU frees a buffer. Measured on 0.1.40.2 with the production config: decode 72.9 tok/s against 71.4 for the 0.1.40.1-based build, prompts unchanged.

What

--resident-budget-gib fills the RAM once from the global expert profile, so a conversation on another topic keeps reading the drive for experts that are cold in the profile but hot for it.

  • --resident-lru-gib L: L GiB of the budget hold the experts the decode reads from the files, least recently used out first (the unbuffered reads' stage pool). The prompt path copies from it but never adds to it or refreshes it: a prompt chunk sweeps every cold expert once and would push out what the decode reuses.
  • STRATA_LRU_KEEP_FREE_GIB=G: the LRU is elastic. A watcher frees the coldest buffers when the available RAM (on Windows also the available commit) falls below G (back to the OS at once, never paged) and grows the pool back a GiB at a time when RAM is free again. It never shrinks below 1 GiB.
  • STRATA_TIER_TRACE=<file>: one line per expert served outside the GPU caches. It is how the policy was chosen: replaying a traced session against same-size policies, the decode-fed LRU cut the decode's drive reads by ~45%.

The serve log reports the LRU's hits and, when elastic, what it holds and what it gave back. Without the flag and the variable nothing changes.

Measured

Full tables and method in bench/results/2026-10-07-windows-ram-tier-lru/. IQ3_S, 2x RTX 5060 Ti, i7-12700K, 64 GB:

  • 10 GiB by profile + 4 GiB LRU: decode 47.0 tok/s, against 41.9 for 14 GiB by profile alone (the same RAM).
  • 12 + 18 GiB elastic: decode ~64-65 tok/s, prompts ~950 / ~1,540 tok/s (8K / 32K).
  • Another program taking 20 GiB gets it in ~6 s, unpaged; decode drops to ~50 and is back at 60-70 tok/s ~20 s after the RAM is freed.

Tried and not in this PR: offering idle buffers to Windows (OfferVirtualMemory) halved decode speed; a routing lookahead reading the predicted blobs tripled the bytes read.

Not tested

The elastic watcher on Linux (it uses MemAvailable; only Windows was measured). HIP.

🤖 Generated with Claude Code

@midhatn

midhatn commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Another Windows data point from a local port of #1323 + #1324 onto v0.1.40.2: Ryzen 9 7940HS, RTX 4070 Laptop 8 GB, 64 GB RAM, IQ3_S, 64K context. I kept the total expert RAM budget at 36 GiB: 32 GiB fixed + 4 GiB LRU versus 36 GiB fixed in the control.

The elasticity worked in a bounded pressure test. With STRATA_LRU_KEEP_FREE_GIB=8, I allocated and touched 2 GiB in 64 MiB steps while the server was loaded. The LRU fell to 1.53 GiB, reported 994 blobs returned, then regrew to 3.08 GiB after release. Available RAM briefly reached 7.79 GiB and was 8.55 GiB at the end of the requests while the allocation was still held. All lookup/tool/cache-return checks passed. This confirms a gradual pressure response, not a hard guarantee against an abrupt allocation.

I haven't established a speed win on this machine. Fresh prompt-read medians, two samples per size:

Prompt tokens 36 GiB fixed 32 + 4 GiB LRU Same LRU, elastic
635 3.54 s 4.11 s 4.08 s
5,914 19.98 s 20.85 s 20.71 s
22,305 73.55 s 74.06 s 73.88 s

So the RAM headroom behavior is useful here, particularly for agents that also compile, while the smaller prompt reads need weighing against it. The port retained the release's stager-wait and stream-ahead policies; this is not a test of the original PR binary. Actual free VRAM stayed at least 353 MiB.

adonizm and others added 12 commits October 7, 2026 19:40
…erts.bin view once reads are unbuffered

NTFS serves unbuffered reads of a mapped file one at a time (WD SN580: 2 MiB reads at QD32
2.35 GB/s mapped vs 3.57 GB/s unmapped). With the file tier unbuffered (--resident-budget-gib
or STRATA_UNBUFFERED_LOAD=1) the mapped view is only a fallback after startup, so it is closed
(STRATA_KEEP_MAPPING=1 keeps it). The prompt path's stager claims runs of consecutive jobs and
reads them with one read_direct batch (STRATA_STAGER_BATCH, default 8 for file-backed blobs).

RTX 5070 Ti, 32 GB RAM, IQ3_XXS, 47.6K prompt, --resident-budget-gib 18 --prefill auto:32768:
1260 -> 1370 tok/s (batch 8) -> 1669 tok/s (view closed).

Ported from Niko1221#833 onto the rewritten main. Two fixes on the way: unmapped_ is declared on every
platform (mapped_blob reads it outside the Windows block, so Linux did not compile), and
copy_blobs asks resident_blob, which follows the exchange rotation (STRATA_EXCHANGE_ROTATE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oss decode windows (experiment)

With unbuffered reads nothing caches a file-tier expert once its stage buffer is reused, three layers
later, so an expert routed again in the next decode window is read from the drive again. Measured on
bestia (IQ3_S, 2x RTX 5060 Ti, --resident-budget-gib 8, unbuffered): a 600-token answer read 8.5-14 GB.
The pool now grows to the given size before it reuses a buffer, least recently used first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of reading the drive again

With STRATA_STAGE_KEEP_MIB the pool keeps experts the decode read from the files; the prompt path's stager
now copies those from memory. It neither admits nor refreshes entries, so a prompt's sweep over every cold
expert does not push out what the decode reuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of the experts the decode reads

The RAM budget was filled once from the global expert profile. Replaying a traced session (IQ3_S, 2x RTX
5060 Ti, six answers on different topics) against other policies of the same size, an LRU fed by the
decode's file reads cut the decode's drive reads by ~45%; measured, 10 GiB by profile + 4 GiB LRU decoded
~12% faster than 14 GiB by profile. The LRU is the unbuffered reads' stage pool; the serve log reports its hits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the GPU caches (measurement)

One line per blob - time, tier (RAM copy or files), decode or prompt path, layer, expert, bytes - so the
RAM tier's choice of experts can be compared offline against other policies on a real workload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b (decode, prompt path, fills)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to Windows instead of keeping them

The LRU part of the RAM budget was ordinary memory: under pressure Windows paged it out, and reading it back
was slower than the drive. Buffers unused for about two decode windows are now offered (OfferVirtualMemory,
low priority): the OS may discard them for other programs, never page them, and a hit reclaims the buffer
first, reading the expert again when its contents are gone. So the LRU can be large while the PC is idle and
shrink by itself when other programs need the RAM. The serve log reports offers, intact and discarded reclaims.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offering each idle LRU buffer to Windows (STRATA_LRU_OFFER) cost half the decode speed: every offer and reclaim
moves hundreds of pages. Instead a watcher checks the available RAM twice a second: below the floor it frees
the coldest LRU buffers (back to the OS at once, nothing paged) and lowers the pool's cap; with RAM free again
it raises the cap a GiB at a time up to --resident-lru-gib. Measured, a 12 + 18 GiB budget with a plain LRU
decoded at 64.0 tok/s against 59.3 for 12 + 6, prompts 958 / 1514 against 768 / 1412 tok/s (8K / 32K).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Squeezed below what one decode window claims at once, the pool allocated and freed its buffers every window:
decode fell to 43 tok/s under memory pressure, reading up to 52 GB per 600-token answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…astic LRU replaces it

Offering idle LRU buffers to Windows halved decode speed (every offer and reclaim moves hundreds of pages); the
elastic LRU gives RAM back when other programs need it without a call per access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, 2x RTX 5060 Ti, 64 GB)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every LRU buffer is committed memory. Watching only the available RAM, the LRU could fill the commit charge up to
its limit (112-120 of 121 GiB while serving, with 8-9 GB of RAM free), and then another program's allocation fails
even with RAM free. The watcher now takes the smaller of the available RAM and the available commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@malloc32

malloc32 commented Oct 7, 2026

Copy link
Copy Markdown
Author

Thanks for porting and testing it, and for the pressure test.

On elasticity: agreed, it is a gradual response, not a guarantee. The watcher polls every 0.5 s and gives back the
deficit plus 512 MiB per step, so an abrupt allocation can briefly outrun it. Here (Windows, 64 GB, elastic 12 + 18
GiB) another program taking 20 GiB got them within 6-7 s without being paged.

On speed: the prompt-read cost you measured is expected and matches what I saw. The prompt path copies from the LRU
when a blob is already there but never adds to it (a prompt chunk sweeps every cold expert once and would push out
what the decode reuses). Taking 4 GiB from the fixed budget for the LRU means the prompt path reads those experts from
the drive instead. Here, 14 GiB fixed against 10 + 4 GiB LRU: prompt 662 -> 629 tok/s at 8K and 1,340 -> 1,282 at 32K
(about -5%), while decode went from 41.9 to 45-47 tok/s. The LRU is aimed at the decode, which your table does not
cover. If you have the time, the comparison that decides it is decode tok/s on a few 600-token answers on different
topics (greedy, thinking off), with the same split of the budget.

Where RAM allows, the LRU can also go on top of the fixed budget instead of being carved out of it; then the prompt
reads stay as they were.

I have pushed two updates to the branch:

  • A rebase onto 0.1.40.2 (the Stager's sleeping waits kept beside the batched reads; the I/O prefetch's buffer flag
    cleared when the LRU frees a buffer). On this PC it decodes 72.9 tok/s against 71.4 for the 0.1.40.1-based build,
    prompt reads unchanged (three interleaved rounds after a warm-up). Your port should no longer be needed: the
    branch itself can be tested on 0.1.40.2.
  • On Windows the watcher now keeps commit available as well as RAM. Every LRU buffer is committed memory, and here
    the commit charge reached 112-120 of 121 GiB while serving with 8-9 GB of RAM free, which can make another
    program's allocation fail with RAM still free.

One Windows finding that may matter on your machine too: with a system-managed page file, the commit limit shrinks
back when memory is released, and the engine clamps --resident-budget-gib to the commit available at its start.
Here it started with 5-11 GiB instead of 12 depending on the moment, and twice failed to page-lock the layer split's
hand-off buffers. A fixed 96 GB page file (commit limit 160 GiB, nothing actually paged) fixed both and alone took
decode from 67-68 to 71 tok/s.

AI-generated. This reply, the rebase and the measurements in it were made by an AI (Claude Opus 5.5, via Claude Code) on the submitter's behalf.

@midhatn

midhatn commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Agreed: my table measures prompt processing, so it does not settle the decode benefit. The short, sampled high-reasoning replies in that run are not a substitute for the sustained multi-topic decode comparison you describe. I would keep the conclusion as “elasticity verified under gradual pressure; decode speed still unqualified on this laptop.”

I checked the startup evidence too: the requested 32 GiB fixed + 4 GiB LRU split was retained, with 67.5 GiB of commit available immediately before the fixed allocation. So startup budget clamping was not the cause of this particular prompt result. I have not benchmarked the new commit-aware watcher at 4b28dc1; the earlier pressure result applies to my previous port. Thanks for separating physical-RAM pressure from commit exhaustion.

Maddoxkkm added a commit to Maddoxkkm/Strata that referenced this pull request Oct 11, 2026
What the release brought for this box (Niko1221#1709 Windows HIP VRAM floor, the HIP stream-capture fix, the
Linux-only PCIe telemetry), the merge table with what was kept and why Niko1221#1439/Niko1221#1841/Niko1221#1656 went, the
measurements (0.1.42+local +5.6% at 16K; the four new PRs +4.4% on top), and the ranked shortlist for
the next cycle (Niko1221#1297 bisect, Niko1221#1909, Niko1221#1324, Niko1221#1167, Niko1221#1298/Niko1221#1151) with the gfx1100 reading list.
Maddoxkkm added a commit to Maddoxkkm/Strata that referenced this pull request Oct 11, 2026
The auto-merge wove in the preamble (stage_keep_ / lru_cap_ / stage_live_), so the conflict was the
allocator: upstream's pinned cudaHostAlloc path is strictly better than the PR's VirtualAlloc, so it stays,
with the LRU's stage_live_ accounting added and the PR's freed-slot reuse deliberately NOT taken (that is
the half that could hand out a slot the watcher freed; it only matters after a shrink). Off by default:
stage_keep_ is 0 and the watcher only starts with the opt-in.
Maddoxkkm added a commit to Maddoxkkm/Strata that referenced this pull request Oct 11, 2026
Maddoxkkm added a commit to Maddoxkkm/Strata that referenced this pull request Oct 11, 2026
Maddoxkkm added a commit to Maddoxkkm/Strata that referenced this pull request Oct 11, 2026
Maddoxkkm added a commit to Maddoxkkm/Strata that referenced this pull request Oct 11, 2026
…10.9%

Six PRs merged individually (Niko1221#1297 Niko1221#1909 Niko1221#1324 Niko1221#1167 Niko1221#1298 Niko1221#1151), none of which changes the default path
(the arms are opt-in and Niko1221#1297 needs a tuning table). Reading the issue tracker produced the real win:
issue Niko1221#1549 documents ring_default_slots() sizing the staging ring at 384 (1024 for a fused ring) when
g_pinned_share >= 0.9, and our STRATA_PREFILL_RING=96 override - left from tuning against a smaller budget
- was costing 16.5% at 4K and 10.9% at 16K. The override is now gone, so the engine's own rule applies.
Measured, with the LRU left off (it buys prefill but eats the resident budget: 11.9 -> 9.1 GiB).
Maddoxkkm added a commit to Maddoxkkm/Strata that referenced this pull request Oct 11, 2026
…p-server.bat fixed

A (in-tree levers):
- Niko1221#1297 kernel tuning: RAN. The tuner kept the DEFAULT shape in all nine cases (3 expert shapes + 6 mmvq
  shapes) and wrote a table with 0 changes - nothing to gain on gfx1100. The tuner also needs its binary
  copied next to the engine on Windows (autotune.py looks for a literal 'tune_decode').
- Niko1221#1151 SIMT scorer: ADOPTED (STRATA_SELECT_SIMT=1) - +3.9% at 16K and +4.5% at 4K on top of the WMMA
  select, measured at matched budgets after the leak below was cleared.
- Niko1221#1324 LRU: off. 2 GiB is a wash (16K -0.8%, budget -1.4 GiB); 6 GiB costs 14% and crushes the budget to 4.9 GiB.
- Niko1221#1167 rocBLAS table: inert on gfx1100 - prompt_f16() is gfx103x-only ('on gfx103x it is ~2x faster').
- A5 hipBLASLt rows: deferred (our 45-row table already beats upstream's 34; the tuner needs a GPU-exclusive run).
B: Niko1221#1838/Niko1221#1803/Niko1221#1876 applied and NEUTRAL (-2..+2% at both sizes, so kept); its conversational test shows no
   measurable difference either.
C: Niko1221#1913 does not reproduce here - per-entry host cost is flat at 0.77-0.86 ms (their box jumps 3-5x).
   Niko1221#1903's placement axis answered: --expert-cache-per-layer costs 3%. Issue Niko1221#1910 does not apply: the
   interleaved 4K pairs put 0.1.42 +2.1% AHEAD of 0.1.41 (923.6/923.5/962.2 vs 958.7/957.4/953.2).
   Niko1221#1906 answered: reuse works, up to 96% of a turn's prompt reused (1286/1341).

Bug found and fixed: stop-server.bat killed a fixed list of old binary names, so newer A/B engines leaked -
one held 11.4 GiB and crushed the next measurement's RAM budget to 0.0 GiB. It now kills any strata*.exe
(matching tasklist) plus tune_decode; that leak is what made the first group-B/SIMT/per-layer readings unusable.
@Niko1221

Copy link
Copy Markdown
Owner

Thanks. We have no Windows machine to test the file tier on, so we cannot gate it. If you can post decode medians on a second Windows PC we will look again.

@midhatn

midhatn commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

I cannot supply the second-PC confirmation from the hardware used for my results above: those measurements all come from the same Ryzen 9 7940HS / RTX 4070 Laptop Windows machine. Repeating on this laptop would not satisfy your independent second-machine gate.

The claim from my existing runs remains limited to elasticity under gradual pressure; sustained decode benefit is still unqualified. I have no new decode medians to add and will keep the second-Windows-PC validation explicitly outstanding.

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.

4 participants