Liquid-Memory KV-Save Failure — Root Cause & Fix
Status: root-caused + host-side fix landed. Real perf restoration needs a fat-station-cuda rebuild + moonshine GPU image redeploy (see "Deploy", below).
Symptom (live)
Probe the live mistral service:
curl -s https://edge.affectively.ai/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"mistral-7b","messages":[{"role":"user","content":"Write a paragraph about the ocean."}],"max_tokens":100}' \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["_gnosis"])'With liquid memory ENABLED the _gnosis block showed:
liquid_kv_saved: falseliquid_miss_reason: "station kv session save failed"liquid_memory: "capture_failed"- generation ~0.58 tok/s (62 tok / 106 s).
With liquid memory DISABLED (THOTH_LIQUID_MEMORY=0, the temporary mitigation
that is currently set on gnosis-openai-mistral-7b) generation runs at the raw
native path ~7 tok/s. So the liquid layer is the cause, not the model.
Architecture (who calls what)
Each model service is one Cloud Run container that runs two processes
(open-source/gnosis/moonshine/docker-entrypoint.sh, openai-server arm):
- The fat-station — a native Rust GGUF inference server on
http://127.0.0.1:8000(the "single-fat-station" upstream). It owns the model weights and the KV cache. - The openai-server (TypeScript host,
distributed-inference-host/src/bin/openai-server.ts) — the OpenAI-compatible front door. It talks to the fat-station over HTTP.
Two decode paths exist on the host:
- Native fast path (
Pipeline.generate→station.generateTokens→ fat-stationPOST /generate). Prefill + decode happen entirely inside the fat-station; KV stays in-process. This is the ~7 tok/s baseline. - Liquid-memory path (
LiquidMemorySessionManager.generate→Pipeline.preparePrefill+generateFromPrefill). The host orchestrates prefill viaPOST /embed+ chunkedPOST /forward, then decodes token-by-token viaPOST /decode-next, and reuses prefixes across requests by saving/restoring the station KV slab viaPOST /kv-session/save|load|delete.
The liquid path's whole value is cross-request prefix KV reuse (and, within a
request, the one-HTTP-call-per-token /decode-next fast decode). When KV-session
save fails, BOTH benefits are lost: no reuse, and the decode falls back to
chooseTokenThroughStations which re-embeds + re-forwards + pulls full logits over
HTTP every token — that is the 0.58 tok/s.
The save wire
- Host
Station.saveKvSession(distributed-inference-host/src/station.ts) doesPOST {FAT_STATION_URL}/kv-session/savewithX-Session-Id+X-Prefix-Lengthheaders. It returnsfalseon HTTP 404 and throws on other non-2xx. LiquidMemorySessionManager.generatewraps that call in.catch(() => false)and, when false, emitsmissReason: "station kv session save failed"andstatus: "capture_failed"(liquid-memory-session.ts).
Root cause
The deployed GPU image runs a STALE fat-station-cuda binary that does not
implement the /kv-session/* (and /decode-next) routes, so every
/kv-session/save returns 404 → host reports capture_failed.
Evidence:
- Deployed image is
moonshine:baked-mistral-7b-v2-gpu(Artifact Registry, created 2026-06-12). Its env hasFLUX_GPU_*flags + the Cloud Run NVIDIA driver, sodocker-entrypoint.sh:select_fat_station_binselectsfat-station-cuda(the cuBLAS L4 backend), notfat-station. fat-station-cudahas no[[bin]]target indistributed-inference/Cargo.tomland is not built by any source-tracked Dockerfile/cloudbuild — it was produced ad-hoc and baked into the GPU image. Its source provenance predates the KV-session routes.- The
/kv-session/save|load|deleteroutes exist ONLY indistributed-inference/src/bin/fat-station.rsandgemma-readout-fix/fat-station.rs. The transport variants (fat-station-uds*,-flow*,-memo,-echo) do NOT have them. A binary built before these routes were added 404s on them. - The route handlers themselves are correct (verified):
handle_kv_session_savereads the prefix KV withget_kv_slaband returns 200; host-orchestrated/forwardchunked prefill genuinely populates the CPUkv_cache.keys/.valuesthatget_kv_slabreads (the GPU only accelerates the GEMM and copies results back to CPU — KV is never GPU-resident, nocudacfg-gates touch the KV cache). So a binary built from current source serves KV-session save at 200 and liquid memory works. The bug is purely a stale deployed binary.
Fix
1. Host: detect KV-session capability and never silently degrade (landed)
distributed-inference-host/src/bin/openai-server.ts:
probeFatStation()now also returnskvSession. NewprobeKvSessionSupport()trusts a/healthkv_session:falseflag, otherwise actively probesPOST /kv-session/save(prefix_len=0) at boot: 404 ⇒ unsupported, anything else ⇒ supported (and it cleans up the probe session).liquidMemoryEnabled = LIQUID_MEMORY_ENABLED && !altArch && stationKvSession. When the station lacks KV sessions, liquid memory is forced OFF and the service serves the fast native/generatepath (~7 tok/s) instead of the broken 0.58 tok/s liquid path. This is self-healing: redeploying a KV-capable binary re-enables liquid automatically (capability is probed live, not pinned by env).- The
/healthserver-info now reports the EFFECTIVE state plus inputs:liquid_memory(effective),liquid_memory_env,station_kv_session.
2. Station: advertise the capability in /health (landed)
distributed-inference/src/bin/fat-station.rs and gemma-readout-fix/fat-station.rs
handle_health now emit "kv_session": true and "decode_next": true. Any binary
that serves /health from this handler also has the routes, so the host's
/health fast-path is truthful and avoids the active probe on capable stations.
3. The actual perf restoration: rebuild fat-station-cuda from current source
The host fix stops the slow-path degradation but, on the current stale GPU image,
liquid memory will be auto-disabled (back to 7 tok/s, not the slow 0.58). To get
liquid memory ON and FAST (KV/prefix reuse, expected to BEAT 7 tok/s), the
fat-station-cuda binary baked into the moonshine GPU image must be rebuilt from
current source (which has the /kv-session/* + /decode-next routes) with
--features cuda, and the image redeployed.
How to verify
After a redeploy with a KV-capable fat-station-cuda and THOTH_LIQUID_MEMORY unset/1:
- Server-info:
Expectcurl -s https://edge.affectively.ai/health | python3 -m json.tool | grep -E 'liquid_memory|station_kv_session'station_kv_session: trueandliquid_memory: true. - Chat probe (same as the symptom probe). Expect in
_gnosis:liquid_kv_saved: true,liquid_memory: "miss"on the first call andliquid_memory: "hit"on a repeat of the SAME prompt, and tok/s ≥ the 7 tok/s native baseline (faster on a repeat prefix). - Headers also carry
X-Thoth-Liquid-KV-Saved: trueandX-Thoth-Liquid-Memory: hit.
If fat-station-cuda is still stale (404 on /kv-session/save), the host now
auto-disables liquid (station_kv_session: false, liquid_memory: false) and
serves the native 7 tok/s path — no 0.58 tok/s degradation.
Deploy
The mitigations currently set on gnosis-openai-mistral-7b
(THOTH_LIQUID_MEMORY=0, MOONSHINE_PREFILL_WINDOWS=0, plus the off-by-default
MOONSHINE_SKYMESH_CACHE/GNOSIS_AMPLITUHEDRON_COORDINATOR) should be revisited
once the rebuilt image is live.
- Rebuild the moonshine GPU image with a current
fat-station-cuda(--features cuda) baked in. The source-tracked CPU cloudbuild isopen-source/gnosis/gemma-readout-fix/cloudbuild.moonshine-baked-mistral-7b.yaml(buildsDockerfile.moonshine, tagmoonshine:baked-mistral7b-tp1). The deployed-v2-gpuimage was built ad-hoc and is NOT reproduced by a tracked cloudbuild — the GPU/cuBLAS build recipe must build thefat-stationbin with--features cudaasfat-station-cudaandCOPYthe currentdistributed-inference-host/src(which carries this host fix). This is a GPU container build; DO NOT trigger it blindly — confirm GPU/build budget first (note: the project's GPU quota is currently saturated; a second concurrent GPU revision cannot be provisioned). - Deploy: push the rebuilt image and update the Cloud Run service to it,
then unset the
THOTH_LIQUID_MEMORYmitigation:gcloud run services update gnosis-openai-mistral-7b \ --project neutral-418500 --region us-central1 \ --image <rebuilt-gpu-image> \ --remove-env-vars THOTH_LIQUID_MEMORY - Verify per "How to verify" above.
Even WITHOUT a rebuild, deploying just the host fix (re-baked into the image's
distributed-inference-host/src) is safe: it turns the broken 0.58 tok/s liquid
path into the 7 tok/s native path until the cuda binary is refreshed.