Concepts

The state engine

Inference engines treat a context as memory inside a process. That is the right model for a chatbot and the wrong one for an agent, because agents do not run forward in a line — they branch, backtrack and resume. Three operations fix it, and they build on each other.

Paging

Positions live in fixed-size pages rather than one flat allocation (kv.DefaultPageTokens = 64). A context is an ordered list of page pointers. This buys two things: a context grows without reallocating, and — far more importantly — two contexts can point at the same page. Everything below depends on that one fact.

CONTEXT — AN ORDERED LIST OF POINTERS [ →p0 ][ →p1 ][ →p2 ][ →p3 ] len 214 positions 64 pos 64 pos 64 pos 22 pos A page is the unit of sharing, of copying, and of eviction. INSIDE ONE PAGE [ layer ][ position ][ kvDim ] — layer-major A forward pass touches one layer at a time across all positions, so this ordering is what keeps the attention loop walking memory forwards instead of striding across it. POSITION-MAJOR ON DISK — SO A SNAPSHOT OUTLIVES THE PAGE SIZE Three precisions, chosen at run time f32 exact · vq8 quantises values only, 66% · q8 quantises keys too, 31%
Layout inside a page is layer-major because that is the order the forward pass reads it in. On disk it is position-major instead, so a cache built with 64-position pages can restore a snapshot written by one using 128.

The prefix tree

Agent traffic is overwhelmingly repetitive: the same system prompt, the same tool definitions, the same conversation re-sent with one more turn on the end. Cache.Match(tokens) walks a radix tree over token sequences and returns a context holding the longest prefix already computed, along with how many tokens matched. Only the new suffix is prefilled.

root "<system prompt> you are…" 2,140 TOKENS · CONTEXT HELD "…tools: [search, write]" split node — no context NOT AN EVICTION VICTIM "…tools: [search]" A query arrives matched: 2,140 of 2,187 tokens prefilled: 47 reported as llamay_cached_prompt_tokens Bounded, and LRU an entry holds page references, so the memory is the cache
Eviction has to walk the tree rather than a list, because an interior node created by an edge split holds no context of its own and must not be considered a victim.

Copy-on-write fork

Context.Fork() copies the page-pointer slice and increments refcounts. Nothing else. A page is cloned only when a branch writes to a page whose refcount is above one — in Context.writable, which is the only place in the package where copying happens.

a branch writes position p refcount of that page? = 1 — NOBODY ELSE HOLDS IT write in place, allocate nothing > 1 — SHARED WITH A PARENT OR SIBLING clone the page, decrement the old one, write to the clone Measured 32-page context, forked 8 ways, one token appended to each branch 0 pages by the fork · ≤16 by the appends A COPYING IMPLEMENTATION WOULD NEED 256
One function copies, and it is the only one. That is what makes the property testable rather than a claim about discipline across four call sites.

Two properties are asserted in tests rather than assumed:

Snapshots

A context serialises position-major rather than page-major, so a snapshot is independent of the page size that produced it. Two checks are non-negotiable and both are in the format itself.

RESTORE, IN ORDER — EACH REFUSAL HAS ITS OWN SENTINEL magic ErrNotSnapshot version ErrSnapshotVersion weight digest ErrSnapshotModel shape ErrSnapshotShape length bound 16M POSITIONS CRC ErrSnapshotCorrupt Why the digest is not advisory KV computed under different weights is not detectably wrong at generation time — it produces fluent, confident nonsense — so it has to fail at load or it never fails at all. Why the length bound exists /v1/contexts/restore hands an unauthenticated body straight in. Four bytes of declared length once bought 24 KB of allocation per page on a 24-layer model. Ids are read before anything is sized.
Files are written through a temporary and renamed, because a partly written snapshot that looks loadable is worse than no snapshot. The on-disk format has not been bumped: nothing a previous build wrote is newly rejected.
91×faster to restore a 512-token context than to re-prefill it — and the ratio grows with length
0 pagesallocated by forking a 32-page context eight ways
1,654 linesof pointer-sharing arithmetic, checked against a reference that shares nothing

How it is proved

The package is pointer arithmetic, and the way it is checked is a reference implementation run beside it in lockstep. The reference is deliberately stupid: every context holds its own full copy of every position, nothing is shared at all, and the prefix tree is a flat list scanned linearly for the longest key that is a prefix of the query. It is obviously correct because it does nothing clever, so a sequence of operations where the two disagree is a bug in the clever one.

TestModelledOperations drives random sequences of every operation the package offers — allocate, extend, overwrite, fork, truncate, close, retain, match, under an arena and without one, at each of the three precisions — and asserts the whole invariant set after every single step:

At 500 seeds of 400 operations against each of the six configurations, all of it holds; the committed run is smaller so the suite stays fast. Around that: forks at a page boundary and one position either side, twelve-deep forks of branches that have themselves been forked and written, every truncation point of a shared context, every single-byte corruption and every truncation of a snapshot, and the concurrency the scheduler actually creates — sixteen goroutines forking one parent, snapshot-and-restore racing a decode that is rewriting the shared prefix, eviction racing allocation — under -race.

Four things that were wrong

Kept here because each one is the reason an invariant above is phrased the way it is.

The bugHow it presentedThe fix
The prefix tree leaked arena slots Nothing looked wrong: counters balanced, pages were collected, the entry bound held. Only the arena went on believing slots were occupied, so a server doing exactly what the tree is for drained its device arena over hours and silently stopped using the accelerated attention path. One release path instead of four hand-written copies. Forty retains against a four-entry bound had left eighty slots out for eight live pages.
Recycled arena slots were handed out dirty The heap path gets zeroed storage from make and the arena path got whatever the last page left, so the same program produced different bytes depending on whether a GPU arena happened to be attached — and an unzeroed slot wrote another conversation's KV into a snapshot. Slots are zeroed on issue. The snapshot is a file whose entire purpose is to be shipped elsewhere, which is what made this more than an aesthetic problem.
Eviction left the node standing Five hundred twelve-token inserts against a four-entry bound left five hundred and one nodes, each holding a full copy of its token edge — tens of megabytes retained for prefixes evicted precisely because nobody wanted them. Eviction prunes upward from the victim, and a node with one child and no context — a split that no longer splits anything — collapses into its child.
Restore believed the length it was told The position count is four bytes; the positions it promises are not. A one-megabyte body could claim several gigabytes before the lie ran into the end of the stream and the checksum that would have caught it. Token ids are read into a plain slice before anything is allocated for them, and a declared length beyond sixteen million positions is refused by name.

What this composes into

What is not done