Skip to the content.

Hardware-aware cache: tiers, zero-copy, per-tier TTL, demote-not-evict

A hardware-aware KV cache is a cache whose placement policy knows the physical character of every tier a cached span can live in — HBM, DRAM, NUMA-far, CXL, disk, remote — and uses it to relocate a hot prefix one tier colder under memory pressure instead of dropping it and paying a full re-prefill later. In fak this is the internal/cachemeta policy and metadata plane: it decides where a KV span should live and when it should move, emitting KVTransfer directives that an engine adapter executes — the plane itself touches no bytes. Its signature move is demote-not-evict: a span demoted to a byte-addressable, coherent tier (NUMA-far or CXL) stays attendable in place and is never recomputed. The demo’s savings are computed by a deterministic cost model over representative tier profiles, not measured on hardware; an operator supplies real numbers for their box.

Status: the policy/metadata plane is shipped and tested (internal/cachemeta, cmd/hwcachedemo). It decides where a cached span should live and when it should move, and emits the existing KVTransfer directives. The physical byte movement (a CXL HDM map, an RDMA transfer, a disk spill) is performed by the engine adapter that consumes those directives — this plane touches no bytes. Numbers in the demo are a deterministic cost model over representative tier profiles, not a hardware measurement; an operator overrides the profiles with values measured for their box.

Why

A KV cache that is co-optimized with the hardware from day one needs to know more than “is this entry hot.” It needs to know the physical character of every place a span can live, and it needs to make the one decision a blind LRU cache cannot: under memory pressure, relocate a hot prefix one tier colder rather than drop it and pay a full re-prefill later.

internal/cachemeta already named where a payload lived (ResidencyTier) and how a KV span moved between tiers (KVTransfer: offload / restore / route / migrate). What it could not express is the part a modern memory hierarchy turns on — the latency, bandwidth, capacity, byte-addressability, and zero-copy shareability of each tier, and a freshness/lifecycle policy fine-grained enough to act per tier. This layer adds exactly that, additively, on the tier-1 foundation plane (no payloads, no new dependencies).

The three pieces

1. The tier model — hardware.go

Two first-class tiers join the ladder between local DRAM and disk:

The full ladder, hottest to coldest:

HBM → DRAM → NUMA-far → CXL → Disk → Remote

Each tier carries a TierProfileReadLatencyNanos, BandwidthMBPerSec, CapacityBytes, ByteAddressable, Coherent, Persistent, and the native zero-copy Share kind. AttendableInPlace() (byte-addressable and coherent) is the property that makes a demote cheap: HBM/DRAM/NUMA-far/CXL are attendable in place, disk/remote are not (a read must stage them back first).

Zero-copy sharing is first-class metadata. A ShareDescriptor on Residency says how a resident payload can be handed to another consumer without a memcpy:

ShareKind meaning
ShareCopy (zero value) must be copied — the fail-safe default
ShareMmap shared mapping, zero-copy across processes on one host
ShareCXLHDM coherent CXL region, zero-copy load/store across sockets/hosts
ShareRDMA RDMA-registered region, zero-copy over the wire by the NIC
ShareDmabuf exported GPU dma-buf, zero-copy GPU↔GPU / GPU↔NIC

The zero value is ShareCopy, so an entry that has not declared a zero-copy capability is never aliased by accident.

2. Per-tier TTL + a multi-state lifecycle — lifecycle.go

A single global TTL answers only “is this stale yet.” A tiered cache needs freshness expressible per tier — a span is cheap to keep in CXL for a long time and expensive to keep in scarce HBM for even a short one — and explicit states so a policy can act on “expired in HBM, demote it” without conflating it with “expired everywhere, drop it.”

Advance decides when a span stops being fresh; placement decides where it then goes. Keeping the two separate is what lets the time policy be tested without a hardware profile and the placement policy without a clock.

3. The hardware-cost-driven placement policy — placement.go

PlanPlacement is the decision a blind LRU cache cannot make. Given the entry’s lifecycle, its size and token count, the tier profiles, and the live per-tier pressure, it returns one of: keep / promote / demote / spill / evict, plus the KVTransfer directive for the move.

The core move is demote-instead-of-evict:

The quantified comparison is RetainCheaperThanRecompute: stage cost into the colder tier vs the recompute cost it avoids. With realistic memory bandwidth, staging almost always wins for a large, expensive prefix — which is exactly why demote-not-evict is the right default. The exception the cost model still gets right: a small span whose only colder home is slow (disk) is cheaper to rebuild than to read back, so it is evicted.

What you can run

go run ./cmd/hwcachedemo

A no-model, no-GPU, deterministic proof that prints the tier ladder with each tier’s character, which tiers share zero-copy, a hot 4000-token prefix relocating one tier at a time under escalating pressure (demote → demote → demote → spill → evict), the cheap-span exception, and the head-to-head tally:

== Blind LRU vs hardware-aware tiering (8 turns sharing a 4000-token prefix) ==
  blind LRU:      re-prefilled 28000 tokens (evict+recompute every reuse)
  tiered (fak):   re-prefilled 0 tokens (demote to dram, stage back)
  -> 28000 prefill tokens saved by demoting instead of evicting

How it connects to the rest of fak

Honest boundaries