Field notes from the cave

ADR-0008: RAM allocator — power-of-two block array + intra-block buddy

Status: Accepted (2026-06-09).

Context

RAM is a sized latency tool, not a savings tool. We want fixed, bounded, fragmentation-resistant RAM that also doubles as the io_uring registered-buffer region.

Decision

Consequences

Revision (2026-07): local-first NUMA regions

--numa NODE makes that node local to the serving threads. --memory fixed-head bytes are mapped there; optional --sub-memory maps that many fixed-head bytes on every other online node. --sub-memory is invalid without explicit --numa, because automatic NIC selection is intentionally not enough authority to commit memory across the entire machine.

Omitting --small-memory preserves the original shared behavior: fixed heads and packed small objects both consume the --memory / --sub-memory regions. Supplying --small-memory creates a strict non-borrowing split. Packed small objects then consume only --small-memory bytes on the local node and, when configured, --small-sub-memory bytes on each other node; fixed heads consume only their original regions. --small-sub-memory requires both --small-memory and explicit --numa. Every nonzero local or subordinate budget is at least one --block and a whole multiple of --block, so the physical HugeTLB-page multiple and ordinary-memory fallback rules apply to both reservations without rounding.

The allocator owns one independently mapped range per region. It attempts to populate each range from the selected node’s explicit hugetlb pool and verifies residency and strict physical placement; if that attempt cannot be satisfied, it maps the same bytes normally, installs mbind(MPOL_BIND | MPOL_F_STATIC_NODES), and mlocks them. Region zero is local. Allocation searches compatible local arenas and unused local blocks before visiting foreign regions. Separate per-region free lists preserve that ordering even when local and foreign blocks are returned in an arbitrary order. A global block ID resolves through a block-to-region table, so physical mappings need not be virtually contiguous.

Once foreign fixed-head blocks are in use, static local-first admission is augmented by score-based promotion (ADR-0019). The maintenance pass may exchange a completely occupied local buddy block with a hotter completely occupied foreign buddy block. It swaps both the bytes and per-block allocator state, then rewrites every affected HeadLoc. Partial blocks and the small-object bump arena are not promotion candidates in this revision, whether their blocks come from the legacy shared reservation or a dedicated split reservation.

The benchmark-only --perverse flag separates CPU/NIC locality from head-memory locality. Serving threads, the promotion coordinator, key index, streaming pools, and inherited default policy stay on the selected serving node. Dense-score workers still bind beside the score arrays they own, while the preferred --memory region—and the preferred --small-memory region, if configured—is mapped on the online node with the greatest NUMA distance from the serving node. The regions remain first in their respective allocation orders, deliberately inverting locality; only fixed-head blocks participate in promotion. This produces a normal/perverse latency A/B with the same serving CPU and PCI path. It is invalid with --no-numa or on a machine with fewer than two online NUMA nodes.

Revision (2026-07): per-class allocators — buddy for large heads, byte-granular arena for small

The buddy min-order was kDeviceBlock (4 KiB) so every head stayed O_DIRECT-aligned. For the large-object workload that’s free — a ram_head-sized head is already a power of two. But once small objects became RAM-only (head is the only copy — ADR-0003), a ~250 B value burned a whole 4 KiB leaf: ~12× worse than memcached on small values. Two facts unlock the fix: a RAM-only head never DMAs (it’s memcpy-filled and io_uring-sent, so it needs no 4 KiB alignment), and a head is movable — reachable only through the index HeadLoc, so its bytes can be relocated as long as that locator is rewritten under the write lock (ADR-0018).

In legacy shared mode a block’s class is fixed on first use, so the two classes never simultaneously occupy one block. In explicit split mode every block’s class is fixed by its pool at startup, and spare capacity cannot cross the pool boundary:

Consequences