AFM Internal Docs

Prefix Cache Architecture

This document describes how AFM stores, restores, guards, and updates prompt-cache state for MLX models. It covers the request path, the radix-tree store, layer-level cache types, the exact-replay safety rules, and the special handling required for recurrent and rotating caches.

Core Idea

AFM caches model-internal layer state, not generated text. The radix tree maps a token prefix to a per-layer cache snapshot.

Fast Path

On a shared prefix, AFM restores layer state, trims it to a safe prefix length, and generates only the remaining suffix.

Safety Guardrails

Exact full-prefix replay is bypassed for recurrent caches, and rotating caches restore metadata differently from simple KV caches.

1. End-to-End Request Flow

Incoming Request Chat messages, parameters, stream mode, model selection.
MLXModelService / BatchScheduler Prepare LMInput, flatten token IDs, decide whether prefix cache is allowed.
RadixTreeCache.findPrefix() Find longest cached token prefix and return saved per-layer state + metadata.
Restore Policy Compute effectiveCachedPrefix. Apply safety bypasses for exact full replay on recurrent caches.
Restore Layer Caches Rehydrate state and metaState, then trim restored layers down to the allowed prefix.
Generate Suffix Only Build a suffix-only input and run MLX generation using the restored cache snapshot.
Emit Response Return content, usage, stream chunks, and cached-token accounting.
RadixTreeCache.insert() Store the new prompt token sequence with fresh per-layer state + metadata.
LRU-Bounded Prefix Store Up to 64 entries by default, evicting least-recently-used cache entries.

2A. Three Architectural Levels, Four Cache Families

1. The Radix Cache

RadixTreeCache is the outer lookup structure. It is not the model's attention cache itself. Its job is only to answer: “Have we seen this token prefix before?”

2. The Live Request Cache

Every request gets a fresh runtime cache stack via context.model.newCache(parameters: ...). On a hit, AFM restores data into this live cache and then continues generation from there.

3. The Per-Layer Cache Classes

Inside the live cache stack, each layer may use a different implementation such as KVCacheSimple, RotatingKVCache, or ArraysCache. These are not separate top-level systems; they are layer-specific cache shapes.

RadixTreeCache Index by prompt-token prefix.
Live generationCache Fresh cache stack for the current request.
Layer 0..N Cache Objects Each layer object can be a different cache class.

Short Version

There is one prefix-cache system. It has three architectural levels: the radix index, the live request cache stack, and the per-layer cache objects. Inside that third level, there are four main cache families.

Why the Distinction Matters

Bugs can happen at different levels: radix lookup bugs, restore bugs in the live stack, or cache-family-specific restore bugs such as the rotating-cache issue fixed for GPT-OSS.

1A. Quick Reference Table

Architectural Levels

Level What It Does Examples
1. Prefix Index Looks up shared prompt-token prefixes. RadixTreeCache
2. Live Request Cache Fresh runtime cache stack for the current generation. generationCache, batch prefill cache
3. Per-Layer Cache Objects Hold the actual model state for each cached layer. KVCacheSimple, RotatingKVCache, MambaCache, CacheList

Cache Families

Family Used For Tensor Layout / Indexing
KVCacheSimple Standard attention KV state. [batch, kv_heads, seq_len, head_dim]; sequence grows on axis 2.
RotatingKVCache Sliding-window / ring-buffer attention. [batch, kv_heads, cache_slots, head_dim]; logical time is tracked by offset and ring cursor idx on axis 2.
ArraysCache / MambaCache Recurrent or state-space layers. No single universal 4D layout. Stores an array-list of tensors; shape is model-specific. MambaCache is a 2-slot ArraysCache.
CacheList Composite wrapper for multiple sub-caches in one logical layer. No single tensor layout. Flattens and delegates indexing/shape semantics to child caches.

2. Main Components

Request-Side Services

  • MLXModelService: direct non-batched generation path.
  • BatchScheduler: batched prefill and generation path.
  • Both own the runtime logic for cache lookup, restore, trim, suffix generation, and cache reinsertion.

Shared Prefix Store

  • RadixTreeCache: token-prefix tree.
  • RadixNode: edge-labeled node with optional cache entry.
  • KVCacheEntry: stores tokens, layerStates, and layerMetaStates.

Per-Layer Cache Types

KVCacheSimple RotatingKVCache ArraysCache MambaCache CacheList

Observed Model Mappings

  • Qwen3.5 A3B: recurrent layers via MambaCache / ArraysCache.
  • Nemotron Nano: mixed recurrent + regular KV layers.
  • GPT-OSS 20B: sliding-window attention via RotatingKVCache.

3. What the Radix Tree Actually Stores

Radix Key

[token_0, token_1, token_2, ..., token_n]

Each path in the tree is a token prefix. Matching is performed against prepared prompt tokens, not raw user text.

Radix Payload

KVCacheEntry { tokens, layerStates, layerMetaStates }

layerStates is the tensor payload from each layer cache. layerMetaStates is the structural metadata required by cache classes such as RotatingKVCache.

Prompt Tokens The exact model-facing tokenized prompt.
Radix Path Shared prefixes collapse into common branches.
Layer States Tensor arrays from cache[i].state.
+
Layer Metadata Structural values from cache[i].metaState.

4. Restore Decision Logic

Step A: Find a Prefix

  • findPrefix(tokens) returns the longest cached prefix.
  • The result includes the matched length and the cached layer payloads.

Step B: Compute a Safe Prefix

  • effectiveCachedPrefix = min(prefixLen, inputCount - 16)
  • This leaves a suffix so the model reprocesses the prompt boundary.
  • For recurrent caches on an exact full replay, AFM bypasses restore entirely.
prefixLen == full input? Exact replay case.
Recurrent Cache Check ArraysCache / CacheList present?
If Yes: exact-replay-bypass Force cold prefill instead of replaying a full recurrent snapshot.

5. Restore Mechanics by Cache Type

Family 1: KVCacheSimple

  • Restore tensor state directly.
  • Trim excess tokens.
  • Physical truncation is safe.

Family 2: Recurrent Array Caches

  • ArraysCache, MambaCache, CacheList.
  • Partial restore is allowed.
  • Exact full replay is bypassed for correctness.

Family 3: RotatingKVCache

  • Needs both tensor state and structural metadata.
  • Restore keep, maxSize, step.
  • Reset live offset/idx to restored state length.
  • Skip physical truncation round-trip.

Family 4: CacheList

CacheList is a composite wrapper around multiple sub-caches for a single logical layer. It is its own cache family structurally, even though it may contain recurrent caches or standard KV caches inside.

If you want the simplest counting scheme, use this one: 3 architectural levels, 4 main cache families.

Dimension and Indexing Map

Family Dim 0 Dim 1 Dim 2 Dim 3 Indexing System
KVCacheSimple batch KV heads sequence length head dimension Linear append-only indexing on axis 2; offset is current logical length.
RotatingKVCache batch KV heads cache slots / time ring head dimension Ring-buffer indexing on axis 2; offset is logical history length, idx is current write cursor, keep reserves sink tokens.
ArraysCache model-specific model-specific model-specific model-specific List of tensors, not one canonical 4D KV layout. Interpretation belongs to the model layer implementation.
MambaCache slot 0 tensor: model-specific slot 1 tensor: model-specific not fixed not fixed Two-tensor ArraysCache; used for state-space / Mamba-style layers.
CacheList child 0 child 1 child-defined child-defined Composite index space; delegates actual dimensional meaning to sub-caches and flattens their state for storage.

5A. What Decides Which Cache Type Is Used?

Primary Decider: The Model Architecture

AFM does not arbitrarily pick cache classes at runtime. The model implementation in MLX defines what each layer needs when it builds caches inside newCache(...) or the model's cache factory.

  • Plain transformer attention layers usually use KVCacheSimple.
  • Sliding-window attention layers use RotatingKVCache.
  • Recurrent or state-space layers use ArraysCache / MambaCache.
  • Composite layers may wrap multiple sub-caches in CacheList.

Secondary Decider: Model Parameters

Some models also change cache shape based on configuration such as sliding-window size or whether a layer is an attention layer versus a recurrent/state-space layer.

In practice, the decisive code lives in the vendor model definitions and cache helpers, especially AFMKit's vendor/MLX/mlx-swift-lm/Libraries/MLXLMCommon/KVCache.swift and the specific model file's makeCache / newCache logic.

Model Definition Example: GPT-OSS, Qwen, Nemotron.
Layer Kind Attention, sliding-window attention, recurrent/state-space, composite.
Cache Class KVCacheSimple, RotatingKVCache, ArraysCache, CacheList.

Example: GPT-OSS 20B

Sliding-window layers choose RotatingKVCache, which is why metadata restore matters there.

Example: Qwen3.5 A3B

Recurrent/state-space layers use array-based caches, which is why exact full replay had to be bypassed.

Example: Standard Attention

Normal transformer attention typically uses KVCacheSimple, the least complicated restore case.

Model-to-Cache Mapping as Coded

Model File Decision Logic Cache Mapping
GPTOSS.swift Iterates model.layerTypes full_attention → standard KV cache
anything else → RotatingKVCache(maxSize: slidingWindow, keep: 0)
Qwen3Next.swift Checks layer.isLinear linear/state-space layer → MambaCache()
attention layer → KVCacheSimple()
NemotronH.swift Uses hybridOverridePattern block type .mambaMambaCache()
.attentionKVCacheSimple()
.mlp / .moe → no cache
BaichuanM1.swift, FalconH1.swift, GLM5MoeDsa.swift Composite layer implementations Use CacheList(...) to bundle multiple sub-caches for one layer.

Full Portfolio Cache-Class Map

Model Cache Rule Family / Output
GPTOSS.swift model.layerTypes: full_attention vs other StandardKVCache or RotatingKVCache
Olmo3.swift args.layerTypes: full_attention vs other KVCacheSimple or RotatingKVCache
Mistral3Text.swift layer.useSliding KVCacheSimple or RotatingKVCache
Gemma3Text.swift Global layer by slidingWindowPattern vs sliding layer StandardKVCache or RotatingKVCache
Gemma3nText.swift layerTypes: full_attention vs sliding_attention StandardKVCache or RotatingKVCache
Exaone4.swift layer.attention.isLocal StandardKVCache or RotatingKVCache
AfMoE.swift layerUsesSliding KVCacheSimple or RotatingKVCache
Qwen3Next.swift layer.isLinear MambaCache or KVCacheSimple
Qwen3_5MoE.swift fullAttentionInterval: linear vs full-attention layer MambaCache or KVCacheSimple
Qwen3_5MoEVL.swift Same text backbone rule as Qwen3_5MoE MambaCache or KVCacheSimple
Jamba.swift layer.isAttn KVCacheSimple or MambaCache
GraniteMoeHybrid.swift configuration.layerTypes == "mamba" MambaCache or KVCacheSimple
LFM2.swift fullAttnIdxs.contains(layerIdx) KVCacheSimple or MambaCache
LFM2MoE.swift fullAttnIdxs.contains(layerIdx) KVCacheSimple or MambaCache
LFM2VL.swift Text backbone uses fullAttnIdxs.contains(layerIdx) KVCacheSimple or MambaCache
NemotronH.swift hybridOverridePattern block type MambaCache, KVCacheSimple, or no cache for .mlp/.moe
FalconH1.swift Fixed composite per layer CacheList(MambaCache(), KVCacheSimple())
BaichuanM1.swift Fixed conv cache plus sliding-or-full attention cache CacheList(MambaCache(), RotatingKVCache or KVCacheSimple)
GLM5MoeDsa.swift Fixed composite per layer CacheList(KVCacheSimple(), KVCacheSimple())
Gemma3.swift (VLM) Delegates to text model with global-vs-sliding rule StandardKVCache or RotatingKVCache
Mistral3.swift (VLM) Delegates to Mistral3Text KVCacheSimple or RotatingKVCache
Pixtral.swift (VLM) Delegates to language model cache factory Depends on wrapped text model
Gemma3nText.swift wrapper
Gemma3.swift wrapper paths
Wrapper newCache just forwards to underlying language model Same cache family mapping as delegated text backbone

6. Known Failure Modes and Why the Current Design Exists

Qwen Exact-Replay Divergence

Recurrent cache layers could produce shortened completions when the cache restore covered the entire prompt. AFM now treats that as unsafe and logs outcome=exact-replay-bypass.

GPT-OSS Rotating-Cache Crash

A rotating cache hit needed metadata restoration, but the original radix store only saved tensor state. A naive metadata restore was also unsafe because ring-buffer cursor values must stay coherent with restored state length. AFM now stores metadata, restores it carefully, and skips unsafe physical truncation for rotating caches.

7. Logging and Observability

PrefixCache Logs

  • outcome=miss
  • outcome=hit
  • outcome=exact-replay-bypass
  • Includes input tokens, cached tokens, suffix tokens, radix entry count, and timing breakdowns.

ChunkStats Logs

  • Preliminary stream usage chunk.
  • Final usage/timing chunk.
  • Used to verify cache-hit accounting in streaming mode.

8. File Map

Repo Files

  • Sources/MacLocalAPI/Models/MLXModelService.swift
  • Sources/MacLocalAPI/Models/BatchScheduler.swift
  • Sources/MacLocalAPI/Models/RadixTreeCache.swift

Vendor Cache Types

  • AFMKit vendor/MLX/mlx-swift-lm/Libraries/MLXLMCommon/KVCache.swift