No-tax disk persistence for GLM-5.3-Flash

A write-behind KV cache on four DGX Spark nodes: the disk tier adds millions of tokens of session context while business-as-usual latency stays indistinguishable from persistence off. What we built, what it measures, and the five design decisions that made it no-tax.
GLM-5.3-Flash 320B MoE EXL3 TR3 4bpw 4× DGX Spark (GB10) vLLM TP4 · DFlash2 k=7 · fp8 KV write-behind disk KV tier 2026-09-13

Headlines

~47M tokens
Total addressable KV cache. 3.39M tokens in GPU (43.2 GiB UMA pool) + a 1 TB-per-rank NVMe quota (≈44M token-equivalents at fp8). Currently warm: ≈6.9M tokens (78.8 GB occupied, 23,120 objects per rank) — old sessions swap out to disk under memory pressure and swap back in on demand. The store has been tested to 78.8 GB; its internal eviction watermarks (800 GB–1 TB) are untested at scale.
3.0 s
200k-session retrieval from cache (200,448 of 204,668 tokens served) versus 112 s cold — 37×. Disk-tier restore, measured at the sizes that converge: an evicted 9k session returns in 1.42 s (341 MB from NVMe) versus ~15 s recompute, and 423,936 tokens restored from disk mid-request inside a live 800k prefill. A full 200k-from-disk cold restore is the one open item (see §5).
0 → ~5%
The entire persistence tax curve. An idle engine writes zero bytes (census-verified), so unpressurized decode is untouched; under a full pool with copies draining, decode measures −4.6%. Cold prefill at 64k matches persistence-off within measurement noise (2,026 vs 2,028 tok/s).

1 · What this is

GLM-5.3-Flash (320B MoE) in EXL3 TR3 4bpw, tensor-parallel across four DGX Spark nodes (GB10, 121.7 GiB unified memory each), vLLM 0.28.1rc1.dev617 on a pinned upstream commit (83252ea899) plus a twelve-patch persistence series, DFlash2 speculative decoding at k=7, --enforce-eager, fp8 KV, MAX_NUM_SEQS=4. The disk tier is per-rank NVMe with a sqlite index and per-rank coordinators. The serving configuration is unchanged from the published TP4 playbook; everything described here is additive.

2 · No disk vs this deployment

persistence OFFwrite-behind ON (this deployment)
Addressable session context3.39M tokens (GPU pool only) ~47M tokens capacity; ~6.9M warm today; sessions survive eviction
Cold prefill 64k / 256k2,028 / 1,901 tok/s 2,026 / 1,919 tok/s — parity (the 256k cell even served 16,128 tokens from disk mid-prefill)
Decode 64k / 256k (C=1)54.5 / 51.0 tok/s 52.5 / 44.5 tok/s — within run noise except deep-context under pressure
200k session, repeat visit112 s every time 3.0 s from cache; 1.42 s from disk at the tested sizes
3.6M-token flood (> GPU pool)overflow recomputes, sessions lost absorbed: 2 GB written, 4–11 declines, zero stalls; evicted sessions restorable — eager mode wrote 74 GB with 228 declines for the same flood
Idle disk traffic— zero (pressure gate) + hourly stale-content flush at background rate

All cells measured with unique salted prompts, streaming TTFT, medians of two reps or better; engine variance ±5–10%. Protocols and probe scripts are in the repo.

3 · The five decisions that made it no-tax

The first version of this tier stored eagerly — every new chunk written on the request path, responses held for the store drain, synchronous all-rank admissions. It worked, and it taxed exactly the traffic it should protect: −11…−38% prefill, −37% decode at 256k, 36× write amplification. The no-tax property came from five decisions, each taken after measurement:

  1. Stores are eviction-driven, never request-driven (patch 0009). The GPU prefix cache is the source of truth for what deserves durability: blocks copy to disk only when the cache evicts them, through a deferred-free sink that holds blocks outside the free queue while background copies drain. Nothing is offered, held, or admitted on any request path — zero store bytes measured at every context size. Demand-aware holding (decline and release under free-queue pressure) keeps the sink from ever stalling allocation — a lesson paid for by a real flood crash.
  2. Reads are RAM-first with a size gate. The connector is consulted only beyond the GPU cache boundary; requests under 32k prompt tokens skip the disk entirely (a small RAM-miss recomputes cheaper than the scan costs). The gate keys on request size, never miss size, and its skips are counted, not silent.
  3. Lookup misses must be nearly free (patches 0010/0010b). The remaining tax was the lookup scan: per-key index reads on four ranks on the scheduler's critical path, −22% prefill at 64k. An in-RAM durable-key index (authoritative for negatives, maintained on every durability transition) plus in-process dispatch for the rank-local coordinator leg took the residual to ~0% (2,026 vs 2,028 tok/s OFF). Unit tests pin both: a counting connection wrapper proves zero index I/O on the fast negative.
  4. An idle engine writes nothing (patch 0011). A free-pool watermark gates the sink: above it, holds are declined. Census-verified zero bytes during a full prefill/decode smoke at OFF-parity speeds; the only measured cost under a full pool is −4.6% decode while copies drain. Stale-content durability moved to its own mechanism rather than weakening this gate.
  5. Stale content flushes in idle time, at decode-zero cost (patch 0012). APC blocks untouched for a configurable age (1 hour default) are written during decode-free steps only — prefill windows and finish drains — as in-place copies fenced against reuse. A decode step never pays for persistence by construction. This is what gives cold sessions durability without memory pressure.

Two engineering disciplines carried the campaign: the patch series is hash-pinned (before/after hashes per file, an AST suite binding the patched methods and running them engine-free), and every measured claim above has a committed probe. The flood-crash and fence-cleanup bugs found in bring-up were caught by that suite or its live smoke — the suite is part of the design, not an afterthought.

4 · Measured evidence

4.1 Prefill and decode across modes

ContextCMetricOFF eager (retired)write-behind
8k1prefill tok/s1,8681,5311,868
64k1prefill tok/s2,0281,5312,026
256k1prefill tok/s1,9011,6911,919
64k1decode tok/s54.546.052.5
256k1decode tok/s51.032.544.5

4.2 The flood and the restore

400 unique 9k sessions (3.6M tokens) against the 3.39M-token pool, then a re-request of an early session while the engine is busy:

eager (retired)write-behind
Written during flood73.8 GB2.07 GB
Allocation declines2284–11
Restore of evicted 9k session4.15 s / 1.03 GB 1.42 s / 341 MB
Engine state afterhealthyhealthy, zero stalls

4.3a · Pressure bench on a small pool (2026-09-14)

KV pool deliberately capped to 944,871 tokens (12 GiB, ~28% of normal) so eviction is continuous. A 200k session (21% of the pool) was seeded, then the fleet carried ten minutes of sustained C=4 production-shape load (rotating unique 8k–32k prefills, 300-token decodes) followed by a 16×120k eviction flood and a cold restart:

PhaseResult
Sustained C=4 under continuous eviction43/43 requests, zero errors; decode median 56.0 tok/s; per-stream prefill 306 tok/s (4-way split)
200k re-request mid-bench109 s (session churned out of the small pool; partial disk service, remainder recomputed)
Eviction flood (16×120k)16/16 OK, engine healthy
200k reload from disk, cold (post-restart)102.9 s — full recompute; zero bytes read from disk (census). On a small, continuously-busy pool the bounded capture window (16k holds / 16 keys-per-step) loses the whole-session race.
Immediate repeat (hot)2.9 s, 200k tokens from cache

Read together with §4.2: whole-session disk restore is proven at 9k (1.42 s) and as mid-prefill partials; at 200k under sustained pressure the honest result is partial-to-none, by the design’s bounded-write economics — the fix is the flush command (§6) or sized windows, not a defect. Concurrency behaviour itself was clean throughout: no stalls, no errors, decode at parity under eviction load.

4.3 Standard benchmark and production smoke

Local Inference Labs standard bench (C1–C6, 700-token outputs, salted prompts): 38.3 / 73.4 / 51.6 / 52.9 / 64.3 / 69.2 aggregate tok/s, zero failures — within variance of the published no-persistence baseline; the tier does not disturb the standard bench.

Production smoke (agentic ladder, 300-token outputs): 32k → 15.4s TTFT, 2,030 prefill tok/s; 128k → 66.6s, 2,008; 256k → 141s, 1,897; 400k (404,745 tokens, the largest single request this fleet has served) → 261s, 1,549 tok/s, decode healthy; a 4×32k concurrent burst ran clean at 393–1,396 prefill tok/s per stream. The configured max_model_len of 850k is refused at admission in 1.1s — the practical single-request ceiling is ~405k tokens, set by unified memory, independent of persistence; whether the baseline playbook shares that wall has not been measured and should be (same ladder, persistence off).

5 · Open item, stated plainly

Full cold restore of large sessions from disk. Disk restore is proven at 9k (1.42 s) and as a 423,936-token partial inside a live 800k prefill, and cache retrieval is proven at 200k (3.0 s). A complete 200k+ session restored cold from disk after a restart recomputes today: the lookup traces show the earliest chunks absent, and the census shows why — under a heavy flood the bounded capture window (4,096 held blocks, 8 keys/step drain) lands gigabytes, not the whole evicted set, and the mamba groups' chunks are visible to the store but not to the flusher's scan population. The dials exist (eviction_hold_cap, eviction_store_per_step, idle_flush_per_scan) and the mechanism is proven in both directions; whole- session coverage at flood scale is engineering, not architecture. Until it lands, quote disk restore at the tested sizes.

6 · Reproduce

# engine: vLLM 83252ea899 + patches/persistence-flash/0001..0013 (hash-pinned manifest;
# AST suite test_native.py, 23 tests, runs engine-free)
# package: persistence/recipe_persistence (bind-mount overlay)
# configs: PERSISTENCE=on  PERSIST_STORE_MODE=evict_only
#   PERSIST_MIN_DISK_LOOKUP_TOKENS=32768  PERSIST_LOOKUP_KEYS_PER_STEP=2048
#   PERSIST_EVICTION_STORE_HIGH_WATERMARK=<~20% pool blocks>   # pressure gate
#   PERSIST_IDLE_FLUSH_STALE_SECONDS=3600 / _PER_SCAN=32 / _SCAN_SECONDS=5
# measure:
python3 benchmark/bench_c1c6.py --url http://<head>:8888 --model <name> --rounds 3
python3 benchmark/ab_matrix.py --modes off,evict --sizes 8192,65536,262144 --conc 1,4
python3 benchmark/probe_evict_refill.py --n 400 --tokens 9000 --settle 45

Public lane: github.com/hughmadden/GLM-5.3-Flash-NVFP4-1M-KV-4x-DGX-Spark under exl3-persistence/. Sydney timestamps throughout.

7 · Credits

Full per-component attribution in the repository's CREDITS.md. The persistence tier — patches, package, probes, this report — is Turquoise Bay AI's additive work, committed with hash-pinned provenance.