Skip to content

Commit bf6400d

Browse files
Tuomo Vendelinclaude
andcommitted
Add Muse-Glimmer-30B on a pinned mainline llama.cpp image
Muse-Glimmer-30B-GGUF is a dense 30B VLM whose architecture landed in mainline llama.cpp on 2026-08-10 (ggml-org/llama.cpp#26841, build b10353). The bonsai image builds PrismML's `prism` fork, whose branch head is 2026-07-31 — ten days older than the architecture — so it cannot load this model at all. Hence a second llama.cpp image rather than a new POOL entry. The two images are not redundant and neither replaces the other: the fork carries the Q2_0_g128 ternary kernels Ternary-Bonsai needs and mainline lacks; mainline carries every architecture merged after the fork diverged. Both share docker/gguf-serve.sh as their entrypoint, so a model moves between them by changing only `image:`. Two entries, because KV turned out to be nearly free on this model (52 layers, num_key_value_heads=2, and a 3:1 sliding/full attention split) — the full native 131072 context costs ~1.82 GiB, so weights, not context, are the constraint: muse-glimmer one 3090, 17gb+vision, 131072 in 1 slot, f16 KV Muse-Glimmer-30B-split both 3090s, dynamic+vision+draft, 131072 in 1 slot, f16 KV Both are ungrouped (new UNGROUPED_GGUF list), so neither generates pairNN co-load pairs: the standby model is not a co-load partner. config.yaml is purely additive — 104 -> 106 models with all pre-existing entries byte-identical and groups unchanged at 45. muse-glimmer is also the on-call standby: ttl 0 (never idle-unload) plus a poller that wakes it once both 3090s have been quiet for an hour. It deliberately does NOT use `persistent: true` — llama-swap defines that as "other groups can never unload this group's members", which would pin the cards and starve every other model. Eviction is already free via the existing exclusive groups, and the wakeup request itself displaces whatever is squatting, so nothing calls the unload API. Also fixes a pre-existing gap: bonsai-image.yml did not trigger on docker/gguf-serve.sh even though Dockerfile.bonsai COPYs it, so a change there would have rebuilt only the mainline image and left the bonsai one silently stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 111bd42 commit bf6400d

12 files changed

Lines changed: 594 additions & 21 deletions

.github/workflows/bonsai-image.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ on:
1010
paths:
1111
- "Dockerfile.bonsai"
1212
- "docker/bonsai-serve.sh"
13+
# Dockerfile.bonsai COPYs this too — without it here, a gguf-serve.sh change
14+
# rebuilds only the mainline image and leaves this one silently stale.
15+
- "docker/gguf-serve.sh"
1316
- ".github/workflows/bonsai-image.yml"
1417
workflow_dispatch:
1518

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: llamacpp-image
2+
3+
# Builds the pinned mainline llama.cpp image (Muse-Glimmer-30B and any future model the
4+
# PrismML fork can't load) and pushes it to GHCR. This is a CUDA compile, so it only runs
5+
# when its own files change or on manual dispatch — NOT on every push.
6+
7+
on:
8+
push:
9+
branches: [main]
10+
paths:
11+
- "Dockerfile.llamacpp"
12+
- "docker/gguf-serve.sh"
13+
- ".github/workflows/llamacpp-image.yml"
14+
workflow_dispatch:
15+
16+
env:
17+
REGISTRY: ghcr.io
18+
IMAGE_NAME: ${{ github.repository_owner }}/llamacpp-mainline # separate package from bonsai
19+
20+
jobs:
21+
build:
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 120
24+
permissions:
25+
contents: read
26+
packages: write
27+
steps:
28+
# The CUDA build produces GBs of object files; the runner's / is only ~14 GB.
29+
# Free extra space AND move Docker's storage to the large /mnt volume (~70 GB)
30+
# so buildkit doesn't fill / mid-build.
31+
- name: Maximize build disk
32+
run: |
33+
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup \
34+
/usr/local/lib/android /opt/hostedtoolcache /usr/share/swift \
35+
/usr/local/share/boost /usr/lib/jvm /usr/share/miniconda || true
36+
sudo swapoff -a || true
37+
sudo systemctl stop docker docker.socket || true
38+
sudo mkdir -p /mnt/docker
39+
echo '{"data-root":"/mnt/docker"}' | sudo tee /etc/docker/daemon.json
40+
sudo systemctl start docker
41+
echo "--- disk ---"; df -h / /mnt; docker info | grep -i "docker root" || true
42+
43+
- name: Checkout
44+
uses: actions/checkout@v4
45+
46+
- name: Set up Buildx
47+
# docker-container driver is required for the GHA layer cache (cache-to/from).
48+
uses: docker/setup-buildx-action@v3
49+
50+
- name: Log in to GHCR
51+
uses: docker/login-action@v3
52+
with:
53+
registry: ${{ env.REGISTRY }}
54+
username: ${{ github.actor }}
55+
password: ${{ secrets.GITHUB_TOKEN }}
56+
57+
- name: Extract image metadata
58+
id: meta
59+
uses: docker/metadata-action@v5
60+
with:
61+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
62+
tags: |
63+
type=raw,value=latest,enable={{is_default_branch}}
64+
type=sha,format=short
65+
66+
- name: Build and push
67+
uses: docker/build-push-action@v6
68+
with:
69+
context: .
70+
file: Dockerfile.llamacpp
71+
push: true
72+
tags: ${{ steps.meta.outputs.tags }}
73+
labels: ${{ steps.meta.outputs.labels }}
74+
cache-from: type=gha
75+
cache-to: type=gha,mode=max
76+
77+
- name: Summary
78+
run: |
79+
echo "### Mainline llama.cpp image pushed :rocket:" >> "$GITHUB_STEP_SUMMARY"
80+
echo '```' >> "$GITHUB_STEP_SUMMARY"
81+
echo "${{ steps.meta.outputs.tags }}" | tr ' ' '\n' >> "$GITHUB_STEP_SUMMARY"
82+
echo '```' >> "$GITHUB_STEP_SUMMARY"
83+
echo "Make this package public too, then it's referenced by config.yaml." >> "$GITHUB_STEP_SUMMARY"

ARCHITECTURE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,29 @@ cache blocks" failure.
138138
| AWQ / safetensors (most models) | **vLLM** `v0.26.0` | `cmd: docker run … vllm/vllm-openai …` (DooD) |
139139
| GGUF, mainstream arch | llama.cpp | bundled `llama-server` child process |
140140
| GGUF, exotic (e.g. `Ternary-Bonsai-27B`) | **PrismML llama.cpp fork** | `cmd: docker run …` of a fork image (custom kernels) |
141+
| GGUF, newer arch (e.g. `Muse-Glimmer-30B`) | **mainline llama.cpp, pinned build** | `cmd: docker run …` of `Dockerfile.llamacpp` |
141142

142143
`Ternary-Bonsai-27B` is a hybrid-attention, multimodal, ternary-quantized model built for
143144
a **PrismML fork of llama.cpp** — vLLM 0.25.1 cannot serve it. This is a concrete reason
144145
the backend-agnostic design matters.
145146

147+
### Why there are TWO llama.cpp images
148+
149+
This is the non-obvious bit. They are not redundant and neither can replace the other:
150+
151+
- `Dockerfile.bonsai` builds **PrismML's `prism` fork**, which carries the Q2_0_g128 ternary
152+
and hybrid-attention CUDA kernels `Ternary-Bonsai-27B` needs. Mainline does not have them.
153+
- `Dockerfile.llamacpp` builds **mainline at a pinned build tag**. The fork's branch head is
154+
2026-07-31, so it predates any architecture merged after that — `Muse-Glimmer-30B` landed
155+
in mainline on 2026-08-10 (`ggml-org/llama.cpp#26841`, build `b10353`) and fails on the
156+
fork with an unknown-architecture error.
157+
158+
Both images share `docker/gguf-serve.sh` as their entrypoint, so moving a model between them
159+
means changing only `image:` in the generated config. Pin the mainline tag rather than
160+
tracking a rolling one, for the same reason the vLLM image is pinned to `v0.26.0`. When
161+
adding a GGUF model, the question to answer first is *which image can actually load it*
162+
check when its architecture was merged against the fork's branch date.
163+
146164
## Request lifecycle
147165

148166
1. Client → `POST /v1/chat/completions` to llama-swap `:9292` with `"model": "<id>"`.

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,11 @@ RUN apt-get update \
2121
# models stays a git push (see .github/workflows/build-and-push.yml).
2222
COPY config.yaml /etc/llama-swap/config/config.yaml
2323

24+
# On-call standby poller. Baked in for the same reason as config.yaml above — the
25+
# docker-compose `oncall-wakeup` service runs this image with an entrypoint override
26+
# rather than bind-mounting the script, so a Portainer Git stack can't mangle it.
27+
COPY scripts/oncall-wakeup.sh /usr/local/bin/oncall-wakeup.sh
28+
RUN chmod +x /usr/local/bin/oncall-wakeup.sh
29+
2430
# Entrypoint/CMD are inherited from the base image; the compose file passes
2531
# --config and --listen.

Dockerfile.llamacpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Mainline llama.cpp image (pinned build) for GGUF models the PrismML fork can't load.
2+
#
3+
# Why this exists ALONGSIDE Dockerfile.bonsai: the bonsai image builds PrismML's `prism`
4+
# fork, which carries the Q2_0_g128 ternary kernels Ternary-Bonsai-27B needs but that
5+
# mainline does not have. The fork's branch head is 2026-07-31, so it predates newer
6+
# architectures — Muse-Glimmer-30B landed in mainline on 2026-08-10 (ggml-org/llama.cpp
7+
# #26841) and needs build b10353+. Neither image can serve both models, hence two.
8+
#
9+
# Pin LLAMACPP_TAG to a build tag, never a rolling one — same policy as the vLLM image
10+
# (vllm/vllm-openai:v0.26.0). The official ghcr.io/ggml-org/llama.cpp:server-cuda package
11+
# only publishes a rolling tag for recent builds, which is why we compile instead.
12+
#
13+
# Built & pushed to GHCR by .github/workflows/llamacpp-image.yml.
14+
# NOTE: Dockerfile inline comments are only valid on their own line (never after an
15+
# instruction like ARG/ENV), so all comments here stand alone.
16+
17+
# ---- build stage -----------------------------------------------------------
18+
FROM nvidia/cuda:12.6.2-devel-ubuntu22.04 AS build
19+
# RTX 3090 = sm_86 (this box's two LLM cards).
20+
ARG CUDA_ARCHS=86
21+
# b10362: 9 builds after Muse Glimmer support (b10353), to pick up immediate fixes.
22+
ARG LLAMACPP_TAG=b10362
23+
ENV DEBIAN_FRONTEND=noninteractive
24+
RUN apt-get update && apt-get install -y --no-install-recommends \
25+
git cmake ninja-build build-essential ca-certificates libcurl4-openssl-dev \
26+
&& rm -rf /var/lib/apt/lists/*
27+
28+
RUN git clone --depth 1 -b ${LLAMACPP_TAG} https://github.com/ggml-org/llama.cpp.git /src
29+
WORKDIR /src
30+
31+
# The build box has no GPU/driver, so executables can't resolve CUDA driver-API
32+
# symbols (cuMemCreate, cuDeviceGet, ...). Link against the CUDA driver STUB the
33+
# devel image ships, exposed as libcuda.so.1 via LIBRARY_PATH. Build ONLY the
34+
# server target. Cap parallelism by RAM to avoid nvcc OOM on smaller runners.
35+
RUN export LIBRARY_PATH="/usr/local/cuda/lib64/stubs${LIBRARY_PATH:+:$LIBRARY_PATH}" \
36+
&& cmake -B build -G Ninja \
37+
-DGGML_CUDA=ON \
38+
-DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS} \
39+
-DCMAKE_BUILD_TYPE=Release \
40+
-DLLAMA_BUILD_TESTS=OFF \
41+
-DCMAKE_SHARED_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda" \
42+
-DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda" \
43+
&& MEM_GB=$(( $(awk '/MemTotal/{print $2}' /proc/meminfo) / 1048576 )) \
44+
&& JOBS=$([ "${MEM_GB:-0}" -lt 16 ] && echo 2 || echo 4) \
45+
&& echo "building llama.cpp ${LLAMACPP_TAG} with -j${JOBS} (host ~${MEM_GB} GB RAM)" \
46+
&& cmake --build build --target llama-server -j "${JOBS}"
47+
48+
# ---- runtime stage ---------------------------------------------------------
49+
FROM nvidia/cuda:12.6.2-runtime-ubuntu22.04
50+
ENV DEBIAN_FRONTEND=noninteractive
51+
# hf CLI: gguf-serve.sh downloads weights with it (this build's llama-server has no
52+
# HTTPS). libcurl4/libgomp1: runtime deps of llama-server. curl: container health tooling.
53+
RUN apt-get update && apt-get install -y --no-install-recommends \
54+
python3 python3-pip libgomp1 libcurl4 ca-certificates curl \
55+
&& pip3 install --no-cache-dir gguf "huggingface_hub[cli]" hf_xet \
56+
&& rm -rf /var/lib/apt/lists/*
57+
58+
# Copy the whole build output dir so the versioned .so SONAME symlinks
59+
# (libggml-cuda.so -> .so.0 -> .so.0.13.1) are preserved for the runtime linker.
60+
COPY --from=build /src/build/bin/ /opt/llamacpp/bin/
61+
ENV PATH="/opt/llamacpp/bin:${PATH}"
62+
ENV LD_LIBRARY_PATH="/opt/llamacpp/bin"
63+
64+
# Same entrypoint the bonsai image uses for its non-ternary GGUF models, so a model
65+
# moves between the two images by changing only the `image:` in config.yaml.
66+
COPY docker/gguf-serve.sh /usr/local/bin/gguf-serve.sh
67+
RUN chmod +x /usr/local/bin/gguf-serve.sh
68+
69+
ENTRYPOINT ["/usr/local/bin/gguf-serve.sh"]

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ static GPU layout** — which is more reliable than dynamic VRAM packing for thi
2323
| `Dockerfile.bonsai` + `docker/bonsai-serve.sh` | PrismML llama.cpp fork image for Ternary-Bonsai-27B (built by CI → GHCR) |
2424
| `.github/workflows/build-and-push.yml` | CI: builds the Dockerfile and pushes the image to GHCR (Portainer can't build from a repo) |
2525
| `.github/workflows/bonsai-image.yml` | CI: builds the PrismML fork image → `ghcr.io/<owner>/bonsai-llama` |
26+
| `Dockerfile.llamacpp` | **Mainline** llama.cpp at a pinned build (`b10362`) — for models the PrismML fork is too old to load (built by CI → GHCR) |
27+
| `.github/workflows/llamacpp-image.yml` | CI: builds the mainline image → `ghcr.io/<owner>/llamacpp-mainline` |
28+
| `docker/gguf-serve.sh` | Shared GGUF entrypoint for **both** llama.cpp images (download-then-serve; vision / drafter / multi-GPU via `GGUF_*` env vars) |
29+
| `scripts/oncall-wakeup.sh` | On-call standby poller — keeps one model warm when the box is idle (see below) |
2630
| `docker-compose.yml` | Portainer stack definition (pulls the pre-built image) |
2731
| `config.yaml` | llama-swap model definitions — GENERATED by `gen_pairs_config.py` (do not hand-edit): standalone models + co-load pairs + solo big models |
2832
| `gen_pairs_config.py` | Generates `config.yaml`: `pairNN.<model>` co-load pairs (one model per 3090) + standalone `<model>` entries. Edit the `POOL`, run `python3 gen_pairs_config.py > config.yaml` |
@@ -198,6 +202,70 @@ repo builds the fork into its own image and points the model at it:
198202
`llama-server` with vision + DSpark speculative + tool calling, on one 3090. It speaks
199203
OpenAI `/v1` + `/health`, so llama-swap proxies it like any other model.
200204

205+
## Muse-Glimmer-30B (mainline llama.cpp backend)
206+
207+
A dense 30B vision-language model (Apache-2.0). It needs **mainline** llama.cpp `b10353+`
208+
— support landed 2026-08-10 — so it **cannot** use the bonsai image, whose `prism` fork
209+
branch is from 2026-07-31, ten days older than the architecture. That's why there are two
210+
llama.cpp images; see ARCHITECTURE → "Backend matrix".
211+
212+
It's cheap on KV: 52 layers, `num_key_value_heads: 2`, and a 3:1 sliding/full attention
213+
split (39 sliding layers windowed at 2048, 13 full). The full layers cost 13 KiB/token and
214+
the sliding ones a flat 78 MiB, so the **entire native 131072 context costs under 2 GiB**.
215+
Weights, not context, are the constraint — which is why two entries exist:
216+
217+
| Model ID | GPUs | Quant | Context | Notes |
218+
|---|---|---|---|---|
219+
| `muse-glimmer` | one 3090 | `17gb` + vision | 131072, 1 slot | the **on-call** model |
220+
| `Muse-Glimmer-30B-split` | both 3090s | `dynamic` + vision + drafter | 131072, 1 slot | best quality; owns both cards |
221+
222+
Both run `-np 1` — the full native context in a single slot, with unquantized f16 KV. Neither
223+
is in `POOL`, so **neither generates `pairNN` co-load pairs**: the standby model is not a
224+
co-load partner, and pairing it would have added 10 pairs nothing would request. They live in
225+
`UNGROUPED_GGUF` in `gen_pairs_config.py`, and the pair count stays at 45.
226+
227+
1. **Build the image** — the `llamacpp-image` workflow (`Dockerfile.llamacpp`) compiles
228+
mainline at the pinned `LLAMACPP_TAG` and pushes `ghcr.io/<owner>/llamacpp-mainline:latest`.
229+
Then **make that GHCR package public** (same as the other images).
230+
2. **Download the weights** on the host (~38 GB, so don't let the first cold start do it):
231+
232+
```bash
233+
HF_TOKEN=hf_... hf download meta-models/Muse-Glimmer-30B-GGUF \
234+
muse-glimmer-30B-kquant-17gb.gguf muse-glimmer-30B-kquant-dynamic.gguf \
235+
mmproj-kquant.gguf dflash-kquant.gguf \
236+
--local-dir /models/hf-cache/gguf/meta-models_Muse-Glimmer-30B-GGUF
237+
```
238+
239+
3. Request either model ID. `gguf-serve.sh` already passes `--jinja`, which the model
240+
requires for its embedded chat template.
241+
242+
The model supports `reasoning_strength` (`low`/`medium`/`high`/`xhigh`). It's left at the
243+
model's own default; to pin it, add `params=dict(reasoning_strength="low")` to the POOL
244+
entry in `gen_pairs_config.py` and regenerate — it emits a `filters.setParams` block.
245+
246+
## On-call standby model
247+
248+
`muse-glimmer` is kept resident whenever the box has stopped working, **without** ever
249+
blocking another model. Three parts:
250+
251+
- Its standalone entry carries **`ttl: 0`** (never idle-unload). Its `pairNN.` members keep
252+
the normal 5 h TTL.
253+
- Every pair/solo group is `exclusive: true`, so **a request to any other model evicts it
254+
immediately**. Nothing has to unload it explicitly.
255+
- The `oncall-wakeup` compose service (`scripts/oncall-wakeup.sh`) polls `/metrics`; when
256+
**both 3090s** sit below `IDLE_PCT` for `IDLE_SECONDS`, it sends a 1-token request, which
257+
evicts whatever is squatting and loads `muse-glimmer`. It matches GPUs by **UUID**, not
258+
index, so a reboot reordering the cards can't make it watch the A2000.
259+
260+
> ⚠️ This is deliberately **not** `persistent: true` — llama-swap defines that as "other
261+
> groups can never unload this group's members", which would pin the cards and starve every
262+
> other model (same trap as the warning above).
263+
264+
Tunable via the service's env vars (`ONCALL_MODEL`, `IDLE_SECONDS`, `IDLE_PCT`,
265+
`POLL_SECONDS`); set `IDLE_SECONDS` low to test, then put it back. **Consequence to accept:**
266+
with the poller running, an idle model gets replaced after ~1 h rather than surviving its
267+
full 5 h TTL. The TTLs still govern unloading; the poller governs replacement.
268+
201269
## Operational notes
202270

203271
- **vLLM crash mitigation:** keep `--enforce-eager` on `Qwen3.6-35B-A3B` until the Xid 31

TODO.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,47 @@ Status legend: `[ ]` todo · `[~]` in progress · `[x]` done
5858
unquantized for this): test SEPARATELY from prefix caching first — the combo has
5959
crashed during cudagraph profiling on hybrid Mamba models — then together.
6060

61+
## Post-deploy verification (Muse-Glimmer-30B + on-call standby)
62+
63+
Config-side checks already pass locally: 106 models / 45 groups, with all 104 pre-existing
64+
entries byte-identical and the two Muse-Glimmer entries purely additive. The rest needs the host.
65+
66+
- [ ] Make the `llamacpp-mainline` GHCR package **public** (or add registry creds in Portainer).
67+
- [ ] Rebuild the **bonsai** image too — both images share `docker/gguf-serve.sh`, which
68+
changed. Without it, `qwythos-v2` / `fablevibes` run an older script than config assumes.
69+
- [ ] Pre-download the ~38 GB of GGUFs (README → Muse-Glimmer) so the first cold start
70+
isn't a multi-GB stall.
71+
- [ ] **Restart llama-swap** — config is read at startup only. `/v1/models` going 104 → 106
72+
confirms the new config was actually picked up.
73+
- [ ] `muse-glimmer` cold start: watch `nvidia-smi` against the budget (16.76 weights +
74+
1.40 mmproj + ~1.2 compute + ~1.82 KV ≈ 21.2 GiB, **~2.1 GiB spare**). This is the
75+
thinnest number in the whole change. It assumes llama.cpp allocates SWA layers windowed,
76+
not full — if it allocates full, KV jumps to ~6.5 GiB and it OOMs. Fallback ladder:
77+
drop `ctx` 131072 → 65536, then `cache_type="q8_0"`.
78+
- [ ] **Coherence prompt** on first load — per the `qwen3.5-9b` precedent above, trust the
79+
output, not a clean startup log.
80+
- [ ] **Vision probe**: send an image part and confirm a grounded description, proving
81+
`--mmproj` actually attached rather than being silently ignored.
82+
- [ ] Context probe near 131k, plus an over-limit request (expect a clean 400).
83+
- [ ] **Stop tokens**: the model card warns never to stop on `<|eom|>` (only
84+
`<|end_of_text|>` / `<|eot|>`). Confirm generations end cleanly and aren't truncated
85+
mid-reasoning; if they are, add explicit EOG handling in `gguf-serve.sh`.
86+
- [ ] Confirm `-np 1` behaviour under load: a second concurrent request should QUEUE behind
87+
the first (llama-server has one slot; `concurrencyLimit: 4` lets llama-swap admit 4), not
88+
429 or share context. If queueing hurts in practice, that is the argument for raising
89+
`par` — at the cost of KV, since each extra slot adds its own 2048 sliding window.
90+
- [ ] `Muse-Glimmer-30B-split`: confirm `-sm layer` spreads across both 3090s, and measure
91+
decode speed — expect ~single-card, since layer-split is pipeline, not tensor, parallel.
92+
If it's *slower* than the single-card entry, the split entry isn't earning its disk.
93+
- [ ] On-call: set `IDLE_SECONDS=120` on the `oncall-wakeup` service, confirm exactly one
94+
wakeup fires and `/running` then shows `muse-glimmer`. Restore `3600`.
95+
- [ ] On-call **yield test**: with `muse-glimmer` resident, request `gemma-26b` and confirm
96+
a clean swap. This is what `persistent: true` would have broken.
97+
- [ ] On-call **no-interrupt test**: start a long generation on another model, confirm the
98+
poller doesn't fire mid-stream (GPU util stays above `IDLE_PCT`, resetting the counter).
99+
- [ ] Decide whether the ~1 h replacement delay is right in practice — it now supersedes
100+
the 5 h TTL for *replacement* (TTLs still govern unloading).
101+
61102
## 1. Build the custom image
62103

63104
- [ ] `Dockerfile` = `FROM ghcr.io/mostlygeek/llama-swap:unified-cuda` + `docker.io`.

0 commit comments

Comments
 (0)