Skip to main content

Module kv_disk

Module kv_disk 

Source
Expand description

The disk tier for the KV prefix cache: where a block goes so that a prefix survives eviction from RAM, and a process restart.

One file per block, named by its BlockHash and sharded into subdirectories by a hex prefix of that hash, so a machine that has cached a million prefixes never puts a million entries in one directory. The payload is the block’s per-layer K and V tensors, flattened, in the cache’s own dtype – no re-encoding, no compression: a KV block is already dense float data, and a compressor would only spend CPU on the request path to lose.

§What a reader is protected from

A cache file outlives the process that wrote it, so every failure mode here is “someone else’s bytes”:

  • A torn write. The publish is temp-file + fsync + rename, which is atomic within a directory on every filesystem ferrox targets – a reader sees the whole file or no file. But a crash mid-write to the temp file, a truncated copy, or a partial restore from a backup can still leave a short file lying around, so the format records its own total length and a SHA-256 of its body. A file that does not match is refused (BlockFormatError), never partially deserialized.
  • A different build. The format is versioned with an explicit readable-set; an unknown version is refused rather than guessed at.
  • A different model or config. That is kv_signature’s job, and this module does not duplicate it: a decoded file becomes an UnverifiedBlock, and only UnverifiedBlock::verify against the reader’s own expectation produces a usable KvBlock.

§The write-ordering invariant

A write is accepted on one thread and finished on another, so for a while a block is “in the store” without being on disk. The rule that makes that safe, and the one every step below is ordered around:

buffer -> index -> queue. A concurrent reader must never see an index hit for a block that has neither a file nor a buffered payload.

So the payload is reachable before anything claims the block exists, and on the way out the file is published before the buffered copy is released. A reader holds the index lock while it consults the buffer, because “this block is not on disk yet” and “here is its payload” have to be one decision – as two, the writer can publish and release in between and the reader finds nothing. When that invariant does break, the reader gets StoreError::MissingPayload rather than a quiet miss: a correctness bug that degrades into a cache miss is a bug nobody ever finds.

The queue is bounded and never drops: a full queue makes the caller write the block itself (DiskStats::inline_writes counts it). Dropping writes silently would be indistinguishable from a cold cache later.

§Layout

<root>/.tmp/<hash>.<pid>.<n>.tmp     in-progress writes
<root>/<hh>/<full-hex>.kvb           published blocks (hh = shard prefix)

§File format (version 2)

magic           8   b"FRXKVBLK"
format_version  4   u32 LE, checked against READABLE_FORMAT_VERSIONS
header_len      4   u32 LE
body_len        8   u64 LE
digest         32   SHA-256 over header || body
header  header_len  block hash, dims, dtype, block layout, model identity
body      body_len  per layer: all K elements, then all V elements

Version 2 added the two block-layout fields – block size and sliding window – and version 1 was dropped from the readable set rather than being read with the window assumed absent. A v1 file cannot say what window it was cut under, and “it did not say” is not “there was none”: see kv_swa for what a mis-aligned block does to an answer. A restart onto this build therefore starts from a cold cache once, and the old files are evicted as unreadable rather than reinterpreted.

The digest covers header and body but not the fixed prefix, so the lengths are checked against the real file size before anything is hashed or parsed – a 4 GB body_len on a 200-byte file is rejected by arithmetic, not by allocating.

Structs§

DecodedBlock
The identity and payload recovered from a block file. The signature is deliberately unverified: use UnverifiedBlock::verify to turn it into a block this process may use.
DiskConfig
How the store is sized, laid out, and how much writing it will do off the calling thread.
DiskKvStore
A content-addressed block store on disk, with its own writer threads.
DiskStats
A snapshot of the tier’s behaviour. Note the two time-valued fields: hit rate alone cannot tell an operator whether a disk hit was cheaper than recomputing the prefix, which is the only question that decides whether the tier is worth having.
ReadHandle
A read that may not have finished yet.

Enums§

BlockFormatError
Why a block file was refused. Every variant means “these bytes are not a block this build can read”, and none of them is recoverable by reading harder.
StoreError
Something went wrong reaching the disk tier. Corruption and incompatibility are not here: those are misses, reported through DiskStats, because a caller’s only sane response to either is to recompute the prefix.

Constants§

BLOCK_FILE_EXT
Extension of a published block file.

Functions§

decode_block
Parses a block file. Checks, in order: length, magic, format version, declared-vs-actual size, checksum, then structure. Nothing is allocated from a length field until that length has been checked against the bytes actually present.
encode_block
Serializes a block. The hash is stored inside the file as well as in its name, so a block found under a wrong or renamed path can still be checked against the identity it claims.
encoded_len
Total on-disk size of a block with this signature, header included.

Type Aliases§

FreeSpaceProbe
Asks how many bytes are still free on the filesystem holding a path. None means “cannot tell”, and the store then trusts only its configured byte budget.
ReadOutcome
What a read produced. Ok(None) is a miss (absent, corrupt, or incompatible); Err is I/O, or the write-ordering invariant breaking.