Skip to content

fix(llama): size llama.cpp's RAM prompt cache to the host (16 GB WSL OOM kills) - #6769

Open
Lightheartdevs wants to merge 3 commits into
mainfrom
claude/llama-cache-ram-host-memory
Open

Lightheartdevs wants to merge 3 commits into
mainfrom
claude/llama-cache-ram-host-memory

Conversation

@Lightheartdevs

Copy link
Copy Markdown
Collaborator

Summary

llama.cpp b9014 keeps earlier prompts, with their context checkpoints, in a host-RAM prompt cache that may grow to 8192 MiB (--cache-ram, LLAMA_ARG_CACHE_RAM). No ODS memory check counts it. The container limit, the VRAM fit and the catalog profiles size only weights, KV cache and checkpoints. Only CPU runtime profiles, plus one 4B NVIDIA profile, set LLAMA_ARG_CACHE_RAM. On the 16 GB WSL laptop, the kernel OOM-killed llama-server three times with the default cache.

This PR makes ODS size the cache for the Docker llama-server when neither the model's runtime profile nor .env already sets a size:

cache_mib = max(512, min(8192, (M - 6 GiB) / 3, L / 4))     # written only when < 8192
  • M: memory Docker can use, in whole GiB. This is the smaller of the host RAM and docker info MemTotal. Inside WSL both readings come from the VM; under Docker Desktop the VM reading is the smaller one.
  • 6 GiB: kept for the rest of ODS and the OS. At the last kill, every other process together held 4.6 GiB of RAM plus 3.6 GiB of swap. At idle after a restart, docker stats shows 3.6 GiB for the other 22 containers.
  • L: the llama-server container limit, LLAMA_SERVER_MEMORY_LIMIT. It comes from the profile, the owner or the NVIDIA default; failing those, the backend's compose default applies (nvidia 64G, cpu 6G, intel/arc 24G). Capping the cache at a quarter of L leaves three quarters of the container for weights, KV cache and checkpoints.
  • Precedence: runtime profile value, then the value already in .env, then the computed default. An owner's value is never overwritten.
  • Where it applies: Linux/WSL installer phase 06 (fresh install, and reruns/upgrades that fill in a missing key), the Windows installer (env-generator.ps1), and dashboard model activation (host agent). Activation computes a size only when the key is absent.
  • Where it does not apply: AMD/Lemonade, native macOS, native Windows llama-server, cloud mode and external LLM. Those paths launch llama.cpp themselves or run none, and this PR leaves them unchanged.
  • At 8192 or more the key stays unset, so large hosts keep llama.cpp's default and their .env does not change.

OOM evidence (laptop, read-only)

Host and configuration

  • Windows 11, 31.4 GB RAM, RTX 5070 Laptop 8 GB.
  • WSL VM: MemTotal 16,083,128 kB (15.3 GiB) plus 4 GiB swap. ODS was installed with the Linux installer inside the WSL distro, and docker info reports MemTotal 16,469,123,072 B.
  • llama-server: Qwen3.5-9B Q4_K_M at ctx 65536, profile nvidia-8gb-64k-q8-kv, image server-cuda-b9014.
  • docker inspect: Memory 12G, MemorySwap 24G. The command line has no --cache-ram, and LLAMA_ARG_CACHE_RAM is passed through unset. The server log reads prompt cache is enabled, size limit: 8192 MiB.

Kernel log (journalctl -k, EDT; UTC is +4 h). Every kill has constraint=CONSTRAINT_NONE ... global_oom: the VM ran out of memory. The container's 12G cgroup limit was never reached.

2026-09-25T18:20:35-04:00 kernel: openclaw invoked oom-killer ...
2026-09-25T18:20:35-04:00 kernel: Out of memory: Killed process 461841 (llama-server) total-vm:107121480kB, anon-rss:9803728kB, file-rss:188kB, shmem-rss:214272kB, UID:0 pgtables:24256kB oom_score_adj:0
2026-09-26T00:15:49-04:00 kernel: python invoked oom-killer ...
2026-09-26T00:15:50-04:00 kernel: Out of memory: Killed process 1070883 (llama-server) total-vm:107100636kB, anon-rss:10049784kB, file-rss:340kB, shmem-rss:206848kB, UID:0 pgtables:24204kB oom_score_adj:0
2026-09-26T02:10:19-04:00 kernel: Out of memory: Killed process 1070883 (llama-server) total-vm:107100636kB, anon-rss:10049784kB, ...   <- same PID and counters as 00:15:50, no "invoked oom-killer": a duplicate journal record, not a 4th kill
2026-09-26T03:36:36-04:00 kernel: chronyd invoked oom-killer ...
2026-09-26T03:36:43-04:00 kernel: Out of memory: Killed process 1356950 (llama-server) total-vm:107644672kB, anon-rss:10113736kB, file-rss:44kB, shmem-rss:212864kB, UID:0 pgtables:25312kB oom_score_adj:0

The 03:36:43 EDT kill (07:36:43Z) made two Pixel sweep prompts fail with "pixel request rejected": the router got a 503 while the model reloaded. From the kernel's task dump at that kill:

Process RSS Swapped
llama-server 10,085 MiB 1,481 MiB
All other processes 4,626 MiB 3,672 MiB

The largest of the others were python3 (1,920 MiB), litellm (625), hermes (385), openclaw (324) and text-embeddings (233). The dump reported Free swap = 0kB.

Why the prompt cache is what grows

b9014 source (tools/server/server-task.{h,cpp}, server-context.cpp):

  • A cache entry holds the slot's full sequence state plus its checkpoints; server_prompt::size() counts both.
  • update() evicts the oldest entries while the total exceeds the limit, but always keeps at least one entry.
  • On a slot switch the order is save, then restore the best match, then evict.

Live sample on the laptop. The model restarted at 08:18Z and a UI phase and sweep were running. I sampled the llama log cache state line and the container cgroup's anon once a minute, read-only:

Time (Z) Cached prompts Cache MiB llama-server anon MiB MemAvailable MiB
08:21 2 101 573 7,974
08:28 4 2,615 3,313 5,612
08:53 11 4,461 4,861 4,116
08:59 14 4,277 4,667 4,320

The cache is most of llama-server's anonymous memory, and it grows toward the 8192 MiB cap as distinct Pixel, Hermes and Open WebUI prompts arrive. The rest is the active slot's checkpoints plus about 0.5 GiB of buffers. At the growth rate in the table, the running laptop is on course for another kill once the cache nears 8 GiB.

Entry sizes from the same log:

Prompt Checkpoints Size in cache
13–20 tokens (short side request) 0 50.5 MiB
5,628 tokens 2 244 MiB
48,465 tokens (Pixel conversation) 32 2,463.8 MiB (855.8 MiB state + 32 × 50.25 MiB checkpoints)
52,019 tokens 32 2,522.9 MiB

Under this memory pressure, saving a 2.5 GiB entry took 9.7 s once and 41.6 s another time (prompt cache update took 9652.90 ms / 41586.09 ms).

Arithmetic at the kill. The fixed host buffers in this configuration are about 0.2–0.6 GiB, and 32 checkpoints are 1.57 GiB. That leaves at least about 7 GiB of the 9.6–10.1 GB anon RSS for the prompt cache. The killed containers' own logs are gone because the container was recreated, so this part is inferred.

On the 16 GB Mac mini, the same default cache reached 8,017 MiB (#6757).

What happens today (before this PR)

Path Sets LLAMA_ARG_CACHE_RAM when
Linux/WSL installer (phase 02 selector or preserved contract → phase 06) Only when a catalog runtime profile sets it: the CPU profiles (512/1024) and qwen3.5-4b nvidia-8gb-64k-q4-kv (2048). The 8 GB-GPU profiles for 9B and Ministral set LLAMA_SERVER_MEMORY_LIMIT=12G or nothing, and no cache size.
Compose (docker-compose.base.yml) Bare pass-through; unset means llama.cpp's 8192.
Dashboard activation (host agent) Only when the new profile sets it; the key is never removed.
Windows installer (env-generator.ps1) Only from the profile (TierConfig); an existing .env value was dropped on rerun.
Native Windows llama-server (AMD Vulkan fallback), native macOS --cache-ram only when .env sets it.

The NVIDIA container default is memory − 3 GiB (<16 GiB) or − 4 GiB (12G on this VM). That is above the VM's real headroom, and compose gives the container 2× that in swap, so the cgroup limit never bounds llama-server here.

What each fleet host class gets

Host M L LLAMA_ARG_CACHE_RAM
tower (128+ GB, NVIDIA) ≥ 30 (about 125) 64G unset → llama.cpp 8192 (no .env change)
laptop (WSL VM 15.7 GB = 15.3 GiB) 15 12G (profile) 3072 (a third of 9 GiB, and a quarter of 12G)
strixy (AMD Strix Halo, Lemonade) – – not ODS-controlled: Lemonade launches llama.cpp with its own arguments; the rule skips amd/Lemonade
Mac (native macOS) – – unchanged here: the native macOS path is separate. Draft #6757 sets --cache-ram 0 (with --parallel 2 --kv-unified --ctx-checkpoints 64) on ≤16 GiB Macs; larger Macs keep 8192

Other classes:

Host class Result
16 GiB native Linux, NVIDIA (L 12G) 3072
24 GiB (20G) 5120
32 GB (31 GiB, 27G) 6912
64 GB running an 8 GB-GPU 12G profile 3072
CPU backend with the 6G compose default 1536
CPU profiles keep their catalog 512/1024

The tradeoff: why not 0 on GPU hosts, and why not 8192 on small ones

With one slot (LLAMA_PARALLEL=1), Pixel interleaves with Open WebUI task calls, Hermes and Talk, and each switch evicts the conversation from the slot. For hybrid models (Qwen3.5/3.6) the RAM cache plus checkpoints is what lets it come back without a full re-prefill.

  • Laptop, RTX 5070, Qwen3.5-9B. Re-prefilling a 29,855-token conversation takes 17–23 s (1,300–1,700 tok/s). Saving it to the cache took 0.7 s with no memory pressure. The live log shows the reuse working: found better prompt with f_keep = 0.352, sim = 0.624 restored the 48,465-token entry for the next Pixel turn.
  • tower1/3, RTX 5090, Qwen3.5-27B, 24k. Returning to a displaced conversation took 1.4 s cached and 7.9 s with --cache-ram 0.
  • Mac mini 16 GB, 9B, interleaved workload. 361.7 s at baseline versus 453.1 s with 64 checkpoints and --cache-ram 0: the foreign chat evicted the main prefix.

Why 3072 rather than a smaller cap in the 16 GB VM. One 64K agent conversation with its 32 checkpoints is about 2.5 GiB. With 3072 the cache keeps that entry plus a few short prompts. A smaller cap such as 2304 still keeps the newest entry, but the next short prompt saved after it pushes the total over the limit, and eviction is oldest-first, so the big entry goes. Replaying the live sequence above with 2304, the found better prompt restore would have missed.

Peak memory. The cache is bounded by max(cap, largest entry) plus one transient new entry, so about 5.5 GiB at worst on the laptop instead of about 10.5 GiB. At the 03:36 kill that is roughly 5 GiB less demand against the 19.3 GiB of VM RAM plus swap.

Container limit: considered, not changed

  • Lowering LLAMA_SERVER_MEMORY_LIMIT toward the VM's headroom would not prevent the crash. The container gets swap equal to its limit (MemorySwap 24G), so llama-server would page, then be cgroup-OOM-killed instead of global-OOM-killed. The kernel already picks llama-server, so the other processes survive either way.
  • The 12G comes from catalog profiles marked fleet-validated (system_ram_min_gb 15). Changing it needs its own validation.
  • Bounding the cache removes the growth that caused the kills. If validation shows other growth, a follow-up can raise the NVIDIA default reserve from 3–4 GiB to the measured stack footprint of about 6 GiB.

Conflicts with open PRs

Tests

Passing locally:

  • bash tests/test-llama-memory-budget.sh: memory-limit parser, the rule table, and phase 06's own block run against fixtures (precedence, Docker VM lower bound, every skipped backend). Mutation-checked: removing the .env fallback or letting AMD through fails it.
  • tests/test-windows-llama-memory-budget.ps1: rule table, fresh NVIDIA install, rerun keeps an owner value, profile wins over .env, 15 GiB VM → 3072, 128 GiB → unset, AMD/cloud/CPU cases. Passes on Windows PowerShell 5.1 (pwsh is not installed here) and is mutation-checked.
  • Both tests are now wired into CI (test-linux.yml, lint-powershell.yml); neither ran in CI before.
  • dashboard-api: new tests/test_llama_cache_budget.py (24), plus 16 activation tests in test_model_activate.py. The activation tests cover the Docker VM lower bound, the container-limit bound, keeping a .env value, profile precedence, large-host unset, the host-RAM fallback, and the skipped runtimes (apple, amd, lemonade, cloud, Windows native).
  • Full dashboard-api suite on Windows, at the first commit: 5686 passed, 19 failed on the branch, against 5647 passed, 20 failed on main. The branch's failures are a subset of main's: Windows-only, or fastapi/symlink/bash-path environment issues. After the second commit changed the fraction, I re-ran the changed test files: test_llama_cache_budget.py 24 passed; test_model_activate.py, test_model_switch_speed.py and test_model_transaction.py had only the 2 failures that also fail on main here.
  • tests/contracts/test-llama-spec-default.py, test-llama-runtime-tunables.py, tests/test-safe-env.sh, tests/test-validate-env.sh and tests/test-cpu-only-path.sh pass.
  • ruff (CI's selection) is clean; shellcheck at error severity is clean.

Not run cleanly here (Windows-only): test-preserve-active-model.py (bash path handling), test-phase06-env-rerun-quoting.sh (docker compose --env-file) and the installer-contracts symlink case. They fail identically on main on this machine, so Linux CI is authoritative for them.

Live validation needed (laptop)

  1. Apply. Rerun the installer from this branch with the active model preserved.
    • .env should contain LLAMA_ARG_CACHE_RAM=3072.
    • docker inspect ods-llama-server should show LLAMA_ARG_CACHE_RAM=3072.
    • The llama log should read prompt cache is enabled, size limit: 3072 MiB.
  2. Activation. Switch to a model whose profile sets its own size (qwen3.5-4b-q4 → 2048), then back. The value follows the profile and is otherwise kept. Deleting the key and then activating should write 3072.
  3. Soak. Run the same int4 sweep plus UI phase that OOM'd.
    • Sample once a minute: cgroup memory.stat anon, the cache state: … (limits: 3072.000 MiB …) line, MemAvailable and SwapFree.
    • Expect llama-server anon to stay at or below about 6 GiB.
    • journalctl -k | grep "Out of memory" must show no new kills.
  4. Prefix reuse. Interleave a Pixel task with an Open WebUI chat. Confirm the found better prompt restores for the Pixel conversation, and that returning to it costs about 1 s rather than a 20 s re-prefill.
  5. Compare sweep pass rate and per-turn latency with the last laptop run (8/10 with 2 OOM ERRORs).

🤖 Generated with Claude Code

Lightheartdevs and others added 3 commits September 26, 2026 04:23
…p's 8 GiB

llama.cpp b9014 keeps earlier prompts (with their context checkpoints) in a
host-RAM prompt cache capped at 8192 MiB by default. No ODS memory check
counts it: the container limit, the VRAM fit and the catalog profiles size
weights, KV cache and checkpoints only, and only CPU runtime profiles set
LLAMA_ARG_CACHE_RAM. In a 16 GB WSL VM the kernel OOM-killed llama-server
at about 10 GB anon RSS three times, dropping Pixel requests while the model
reloaded.

When neither the model's runtime profile nor .env sets it, the Linux
installer (phase 06), the Windows installer and dashboard model activation
now write a size for the Docker llama-server: a quarter of the memory left
after 6 GiB for the rest of ODS (Docker's memory when smaller than the
host's), at most a quarter of the llama-server container limit, at least
512 MiB. At 8192 MiB or more the key stays unset, so large hosts keep
llama.cpp's default. An existing value is kept; a profile's value wins.
Lemonade (AMD) and native macOS are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live sample on the 16 GB WSL laptop (Qwen3.5-9B, 64K, b9014) showed
that a Pixel conversation is cached with all its context checkpoints: a
48,465-token prompt with 32 checkpoints took 2,464 MiB, a 52,019-token one
2,523 MiB. A quarter of the headroom (2304 MiB) is below one such entry,
so the next short prompt saved after it evicts it (b9014 evicts
oldest-first once the total exceeds the limit). A third of the memory
left after 6 GiB gives 3072 MiB in that VM, which is also a quarter of
the 12G container limit of its runtime profile: one full agent
conversation plus a few short prompts. Hosts from about 30 GiB keep
llama.cpp's default unless their container limit is smaller.

Also keeps the Windows test's new assertions away from the lines #6617
changes, and restores LF endings in the new Python test.

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

Copy link
Copy Markdown

Sensitive Files Detected

Trigger: Security-sensitive files detected: ods/installers/

Files flagged:

ods/installers/lib/llama-memory-budget.sh
ods/installers/phases/06-directories.sh
ods/installers/windows/lib/env-generator.ps1

Extra human review is recommended for this PR.


Claude Code Review | Sensitive File Detection | ~$1.50

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.

1 participant