Skip to main content
Forks are not free-floating: the daemon organizes them in a snapshot tree, where each child is a delta against its parent. This is the shape RL exploration wants — branch the environment at decision points, explore each branch independently, keep the winners, prune the losers:
A tree’s payload is either a single VM or a whole cluster (N member VMs forked, suspended, and resumed as one unit — see Cluster mode). Any live node is a fork source, so trees go as deep as the search does; the daemon reports real parent edges.

Node lifecycle

Full request/response shapes: Daemon API.

Keyframes

A node that is a fork source needs a keyframe — a memory.bin on disk that its children’s MAP_PRIVATE mappings are views of (CoW fork). Roots get theirs at create/adopt time, and that one is a full dump (~7.8 s for a 3 × 2 GiB Ubuntu cluster; there is no ancestor to clone). Children are cheaper. Forking from a child clones the nearest ancestor’s memory.bin with a filesystem reflink (FICLONE, ~100 µs) and overwrites only the pages that node has dirtied since it was forked. The keyframe costs O(divergence), not O(guest RAM): Two things to know: the disk figure is the workload’s, not the clone’s — three k3s nodes dirty a fifth of guest RAM in 30 s of doing nothing, and nothing on this path can make a busy node’s keyframe small. And a reflinked file lies to du and stat (both count shared extents as if the child owned them), so the daemon records the true physical cost at write time — base_disk_bytes on the tree_prepared reply and in tree_nodes — and the disk budget counts that. A child whose tree root is not on a reflink filesystem falls back to the full dump, loudly (wrote_base_incremental: false). A child does not need its own keyframe to be forked from later if an ancestor’s suffices — has_base_snapshot in tree_nodes tells you which nodes own one.

Budgets

The agent decides what to keep; k7d enforces budgets so the tree doesn’t eat the machine. Every tree carries a TreeBudget: When a tree_create* request omits the budget, the daemon applies a documented default sized for a node running a handful of 256 MiB guests: 8 live VMs, 8 GiB RAM, 32 GiB disk, chain depth 1. k7d quickstart passes its own (32 VMs, 16 GiB, 64 GiB, depth 8) rather than letting the default reject a 4-branch batch. The budget also sizes the tree’s cgroup jail: pids and memory caps derive from it, so there are no separate knobs to keep in sync.

LRU eviction

Under budget pressure, the daemon suspends the least-recently-active unprotected nodes: their delta goes to disk and their RAM is freed. Suspended nodes can be resumed later. Proactive suspension kicks in at 85% of the RAM budget — kept well below OOM territory and kernel reclaim stalls — and before a keyframe is written the daemon makes disk headroom the same way. tree_protect pins a node (a winner you cannot afford to lose) so budget pressure can’t touch it. Eviction never deletes state — only tree_prune does. The budget/eviction bookkeeping is one of the two formally verified pieces of k7d: the model is translated to Lean via Aeneas and its invariants are machine-checked. See Formal verification.

Node metadata

tree_nodes returns every node’s metadata (NodeInfo): id, parent, children, state, label, dirty_page_count (pages this node’s delta captures; Σ over members for a cluster), has_base_snapshot (the node owns a keyframe), base_disk_bytes, created_at_unix, last_active_at_unix (what LRU ranks on), protected, payload kind (Vm or Cluster), and vm_count — plus, alongside the list, the tree’s live usage against its budget, live_guest_cids per live node, and live_dilation per live node. tree_list carries usage and budget per tree. tree_watch delivers the same snapshot once, then one event per mutation with the daemon’s latency_ns for forks.