metrale_storage/kv_paging/
ns.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: KV paging namespace and wire-key derivation, and the strict parsers for
4//! the `METRALE_KV_PAGING*` variables.
5//!
6//! Owner: storage, KV paging tier.
7//! Invariants:
8//! - Every namespace returned is non-zero: `derive_kv_ns` falls back to `FNV_OFFSET`
9//!   and an override of 0 is an error.
10//! - The namespace is a pure function of its six arguments, encoded as the tagged
11//!   records listed on [`derive_kv_ns`].
12//! - The parsers never default: junk input is an error.
13//!
14//! The paging peer keys its residency by the u64 the client sends
15//! (`snapshot_swap::run_paging_loop_shared`), so the namespace folded into that key
16//! is what separates two models, and two clients of one model, on one peer. A KV
17//! `GroupKey.block` is a client-local disk-block index
18//! (`HighSpeedSwap::alloc_disk_block_id`), so two clients of one model use the same
19//! block ids for unrelated data. The namespace therefore folds a per-client salt,
20//! random per connect unless `METRALE_KV_PAGING_SALT` pins it: clients share peer
21//! capacity, not cached blocks.
22//!
23//! The model fingerprint does not encode the KV cache dtype or block layout, and
24//! `GroupLayout::group_id` depends on `num_blocks`, so the namespace folds the full
25//! layout next to the fingerprint. The fingerprint arrives as a plain u64
26//! (`ModelDims::model_fp`); this crate does not depend on the model config.
27
28use std::num::NonZeroU64;
29
30use anyhow::{Result, anyhow};
31
32use crate::group::GroupLayout;
33
34/// 2026-09-25: Encoding version, hashed as the tag-0x00 record. Changing it changes
35/// every KV namespace.
36pub const KV_NS_VERSION: u64 = 1;
37
38/// 2026-09-25: Domain separator hashed into every KV namespace as the tag-0x02
39/// record, so KV keys differ from SSM keys in the key material as well as by the
40/// peer's per-`(kind, blob_bytes)` arena. ASCII `"KV"` + `"PAGE"` + 1.
41pub const KV_DOMAIN: u64 = 0x4B56_5041_4745_0001;
42
43pub(crate) use crate::tier::hash::{FNV_OFFSET, fnv1a_64};
44
45pub(crate) use crate::tier::hash::mix64;
46
47fn put_u64(buf: &mut Vec<u8>, tag: u8, v: u64) {
48    buf.push(tag);
49    buf.extend_from_slice(&v.to_le_bytes());
50}
51
52/// 2026-09-25: Derive the KV paging namespace: FNV-1a/64 over fixed-width
53/// `[tag][8-byte LE]` records. A change to the fields or their order must bump
54/// [`KV_NS_VERSION`]:
55///
56/// | tag  | field           | tag  | field          | tag  | field         |
57/// |------|-----------------|------|----------------|------|---------------|
58/// | 0x00 | KV_NS_VERSION   | 0x04 | block_size     | 0x08 | num_kv_heads  |
59/// | 0x01 | model_fp        | 0x05 | head_dim       | 0x09 | fs_block_size |
60/// | 0x02 | KV_DOMAIN       | 0x06 | num_layers     | 0x0a | group_stride  |
61/// | 0x03 | elem_bytes      | 0x07 | num_blocks     | 0x0b | client_salt   |
62///
63/// `model_fp` is `ModelFingerprint::derive_kv` in metrale-model-engine: model type,
64/// quantization identity, `METRALE_MODEL_ID` and geometry. A hash of 0 is replaced by
65/// `FNV_OFFSET`.
66pub fn derive_kv_ns(
67    model_fp: u64,
68    layout: &GroupLayout,
69    elem_bytes: u32,
70    block_size: u32,
71    head_dim: u32,
72    client_salt: u64,
73) -> NonZeroU64 {
74    let mut buf = Vec::with_capacity(12 * 9);
75    for (tag, v) in [
76        (0x00u8, KV_NS_VERSION),
77        (0x01, model_fp),
78        (0x02, KV_DOMAIN),
79        (0x03, elem_bytes as u64),
80        (0x04, block_size as u64),
81        (0x05, head_dim as u64),
82        (0x06, layout.num_layers as u64),
83        (0x07, layout.num_blocks as u64),
84        (0x08, layout.num_kv_heads as u64),
85        (0x09, layout.fs_block_size),
86        (0x0a, layout.group_stride),
87        (0x0b, client_salt),
88    ] {
89        put_u64(&mut buf, tag, v);
90    }
91    let h = fnv1a_64(&buf);
92    NonZeroU64::new(h).unwrap_or(NonZeroU64::new(FNV_OFFSET).expect("FNV offset is non-zero"))
93}
94
95/// 2026-09-25: Wire key for one KV block: `mix64(base_group_id, ns)`, the fold the SSM
96/// tier's `PagingSnapshotStore::wire` also uses. For a fixed namespace it is a
97/// bijection, so distinct group ids never share a key. The caller passes the id of
98/// the block's first group, `group_id(GroupKey::new(layer, block, 0, K))`.
99pub fn wire_key(ns: NonZeroU64, base_group_id: u64) -> u64 {
100    mix64(base_group_id, ns.get())
101}
102
103/// 2026-09-25: Parse a u64 in decimal or `0x`/`0X` hex, after trimming; anything
104/// else is an error naming `var`.
105pub fn parse_u64_strict(var: &str, raw: &str) -> Result<u64> {
106    let s = raw.trim();
107    let parsed = match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
108        Some(hex) => u64::from_str_radix(hex, 16),
109        None => s.parse::<u64>(),
110    };
111    parsed.map_err(|e| anyhow!("{var}={raw:?} is not a valid u64 (decimal or 0x-hex): {e}"))
112}
113
114/// 2026-09-25: Resolve the `METRALE_KV_PAGING_NS` value: `None` returns `derived`; a
115/// value is parsed strictly and 0 is an error.
116pub fn resolve_kv_ns_from(override_raw: Option<&str>, derived: NonZeroU64) -> Result<NonZeroU64> {
117    match override_raw {
118        None => Ok(derived),
119        Some(raw) => {
120            let v = parse_u64_strict("METRALE_KV_PAGING_NS", raw)?;
121            NonZeroU64::new(v).ok_or_else(|| {
122                anyhow!(
123                    "METRALE_KV_PAGING_NS=0 is invalid: ns=0 is unrepresentable (it would \
124                     cross-serve KV state on a shared peer); unset it to use the derived \
125                     namespace (logged at INFO on connect)"
126                )
127            })
128        }
129    }
130}
131
132/// 2026-09-25: Resolve the `METRALE_KV_PAGING_SALT` value: `None` returns `Ok(None)`,
133/// and the caller draws a random salt; a value is parsed strictly.
134pub fn resolve_salt_from(raw: Option<&str>) -> Result<Option<u64>> {
135    raw.map(|r| parse_u64_strict("METRALE_KV_PAGING_SALT", r))
136        .transpose()
137}
138
139/// 2026-09-25: Resolve the `METRALE_KV_PAGING` value after trimming: unset or `0`
140/// is `false` (the raw one-sided `RdmaKvBackend`), `1` is `true` (the paging
141/// backend), anything else is an error.
142pub fn kv_paging_selected(raw: Option<&str>) -> Result<bool> {
143    match raw.map(str::trim) {
144        None | Some("0") => Ok(false),
145        Some("1") => Ok(true),
146        Some(other) => Err(anyhow!(
147            "METRALE_KV_PAGING={other:?} is invalid: 1 = peer-owned paging KV, 0/unset = the \
148             raw one-sided KV blade"
149        )),
150    }
151}
152
153/// 2026-09-25: Resolve `METRALE_KV_PAGING_ARENA_GB`, the peer arena size in GiB
154/// (fractional accepted), floored to a multiple of `block_bytes`. Unset, non-finite,
155/// not above 0, or smaller than one block is an error. The raw backend sizes its
156/// arena to hold every group (`RdmaKvBackend::connect`); the paging arena has no
157/// such size to derive, so it must be given.
158pub fn resolve_arena_bytes_from(raw: Option<&str>, block_bytes: u64) -> Result<u64> {
159    let raw = raw.ok_or_else(|| {
160        anyhow!(
161            "METRALE_KV_PAGING=1 requires METRALE_KV_PAGING_ARENA_GB (peer warm-arena size in \
162             GiB, fractional ok) — explicit config or fail fast (PCND)"
163        )
164    })?;
165    let gb: f64 = raw
166        .trim()
167        .parse()
168        .map_err(|e| anyhow!("METRALE_KV_PAGING_ARENA_GB={raw:?} is not a number: {e}"))?;
169    if !gb.is_finite() || gb <= 0.0 {
170        return Err(anyhow!(
171            "METRALE_KV_PAGING_ARENA_GB={raw:?} must be a finite value > 0"
172        ));
173    }
174    let bb = block_bytes.max(1);
175    let arena = ((gb * (1u64 << 30) as f64) as u64 / bb) * bb;
176    if arena == 0 {
177        return Err(anyhow!(
178            "METRALE_KV_PAGING_ARENA_GB={raw} is smaller than one KV block ({bb} B) — the \
179             warm arena must hold at least one block"
180        ));
181    }
182    Ok(arena)
183}
184
185/// 2026-09-25: `true` when `kv_peer_set` and `flag_raw` selects the paging backend;
186/// an invalid `flag_raw` is an error. `CascadeBackend` evicts through per-head
187/// `write_from_host`, which `KvPagingBackend` refuses.
188pub fn cascade_conflicts_with_paging(kv_peer_set: bool, flag_raw: Option<&str>) -> Result<bool> {
189    Ok(kv_peer_set && kv_paging_selected(flag_raw)?)
190}
191
192#[cfg(test)]
193#[path = "ns_tests.rs"]
194mod tests;