Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 94 additions & 35 deletions crates/core/src/engine/cache/radix_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use rustc_hash::FxHashMap;
use slotmap::{SlotMap, new_key_type};
use std::cmp::Reverse;
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::Instant;

new_key_type! {
Expand Down Expand Up @@ -169,9 +170,11 @@ pub struct TreeNode {
/// page identity so completed radix state does not retain token IDs.
/// Consequently, as in router-side indexing, hash collisions are treated
/// as identical pages rather than guarded by an exact-token comparison.
pub key: Vec<LocalBlockHash>,
/// Admission snapshots share edge contents. Copy only an edge that changes
/// while a snapshot is live; normal decode extensions retain their capacity.
pub key: Arc<Vec<LocalBlockHash>>,
/// One physical page ID per key. Length = `key.len()`.
pub value: Vec<KvPageId>,
pub value: Arc<Vec<KvPageId>>,
/// Walk-to-root reference count (protected when > 0).
pub lock_ref: usize,
/// Monotonic timestamp for LRU eviction.
Expand Down Expand Up @@ -207,6 +210,30 @@ struct BeladyLeaves {
indexed: FxHashMap<NodeId, (SequenceHash, usize)>,
}

/// Leave the suffix in `edge` and return a compact prefix. When a snapshot
/// shares the edge, copy each half once instead of cloning before splitting.
fn split_edge_prefix<T: Clone>(edge: &mut Arc<Vec<T>>, at: usize) -> Vec<T> {
if let Some(values) = Arc::get_mut(edge) {
let suffix = values.split_off(at);
let mut prefix = std::mem::replace(values, suffix);
// The short prefix must not retain the original edge's allocation.
prefix.shrink_to_fit();
prefix
} else {
let prefix = edge[..at].to_vec();
*edge = Arc::new(edge[at..].to_vec());
prefix
}
}

fn truncate_edge<T: Clone>(edge: &mut Arc<Vec<T>>, len: usize) {
if let Some(values) = Arc::get_mut(edge) {
values.truncate(len);
} else {
*edge = Arc::new(edge[..len].to_vec());
}
}

impl RadixCache {
/// Snapshot allocator and radix metadata for a fallible admission transaction.
/// This is not a second owner of live request leases; only one cache state may commit.
Expand All @@ -231,8 +258,8 @@ impl RadixCache {
let root = nodes.insert(TreeNode {
children: FxHashMap::default(),
parent: None,
key: Vec::new(),
value: Vec::new(),
key: Arc::default(),
value: Arc::default(),
lock_ref: 0,
last_access_time: Instant::now(),
});
Expand Down Expand Up @@ -810,8 +837,8 @@ impl RadixCache {
node.last_access_time = now;
debug_assert!(node.children.is_empty());
debug_assert!(node.lock_ref <= 1);
node.key.extend_from_slice(key);
node.value.extend_from_slice(value);
Arc::make_mut(&mut node.key).extend_from_slice(key);
Arc::make_mut(&mut node.value).extend_from_slice(value);
if node.lock_ref == 0 {
self.evictable_size += key.len() * self.page_size;
} else {
Expand All @@ -830,13 +857,8 @@ impl RadixCache {
let child = &mut self.nodes[child_id];
let child_parent = child.parent;
let original_ck = child.key[0];
let suffix_key = child.key.split_off(split_pos);
let mut prefix_key = std::mem::replace(&mut child.key, suffix_key);
let suffix_value = child.value.split_off(split_pos);
let mut prefix_value = std::mem::replace(&mut child.value, suffix_value);
// The short prefix must not retain the original edge's allocation.
prefix_key.shrink_to_fit();
prefix_value.shrink_to_fit();
let prefix_key = split_edge_prefix(&mut child.key, split_pos);
let prefix_value = split_edge_prefix(&mut child.value, split_pos);
let suffix_ck = child.key[0];
(
child_parent,
Expand All @@ -855,8 +877,8 @@ impl RadixCache {
let intermediate = TreeNode {
children: inter_children,
parent: child_parent,
key: prefix_key,
value: prefix_value,
key: Arc::new(prefix_key),
value: Arc::new(prefix_value),
lock_ref,
last_access_time: accessed,
};
Expand Down Expand Up @@ -886,8 +908,8 @@ impl RadixCache {
let new_node = TreeNode {
children: FxHashMap::default(),
parent: Some(parent_id),
key: key.to_vec(),
value: value.to_vec(),
key: Arc::new(key.to_vec()),
value: Arc::new(value.to_vec()),
lock_ref: 0,
last_access_time: now,
};
Expand Down Expand Up @@ -1015,7 +1037,7 @@ impl RadixCache {
let split_pos = victim_pages - eviction_pages;
let (nodes, page_pool) = (&mut self.nodes, &mut self.page_pool);
let victim_node = &mut nodes[victim_id];
victim_node.key.truncate(split_pos);
truncate_edge(&mut victim_node.key, split_pos);
let evicted_values = &victim_node.value[split_pos..];
if let Some(belady) = &mut self.belady {
for page in evicted_values {
Expand All @@ -1024,7 +1046,7 @@ impl RadixCache {
}
page_pool.free_pages(evicted_values);
evicted_indices.extend_from_slice(evicted_values);
victim_node.value.truncate(split_pos);
truncate_edge(&mut victim_node.value, split_pos);

self.evictable_size -= eviction_len;
evicted += eviction_len;
Expand All @@ -1044,7 +1066,7 @@ impl RadixCache {

evicted_indices.extend_from_slice(&victim_node.value);
if let Some(belady) = &mut self.belady {
for page in &victim_node.value {
for page in victim_node.value.iter() {
belady.page_hashes[page.index()] = None;
}
}
Expand Down Expand Up @@ -1097,20 +1119,29 @@ mod tests {
.unwrap()
}

#[test]
fn belady_reranks_compressed_tail_before_evicting_its_prefix() {
#[rstest::rstest]
fn belady_reranks_compressed_tail_before_evicting_its_prefix(
#[values(false, true)] shared: bool,
) {
let mut cache = RadixCache::new(3, 1);
cache.set_belady_oracle(oracle_for_prompts(&[&[1], &[3], &[1, 2]]));
let ab = cache.page_pool.allocate(2).unwrap();
cache.insert(&[1, 2], &ab);
let c = cache.page_pool.allocate(1).unwrap();
cache.insert(&[3], &c);
let snapshot = shared.then(|| cache.admission_checkpoint());

let (tokens, pages) = cache.evict(2);
assert_eq!((tokens, pages), (2, vec![KvPageId(1), KvPageId(2)]));
assert_eq!(cache.prefix_match_len(&[1, 2]), 1);
assert_eq!(cache.prefix_match_len(&[3]), 0);
assert_eq!(cache.available_tokens(), 2);
if let Some(mut saved) = snapshot {
assert_eq!(saved.prefix_match_len(&[1, 2]), 2);
assert_eq!(saved.prefix_match_len(&[3]), 1);
assert_eq!(saved.available_tokens(), 0);
assert_eq!(saved.evict(2).1, vec![KvPageId(1), KvPageId(2)]);
}
}

#[test]
Expand Down Expand Up @@ -1239,9 +1270,9 @@ mod tests {
let (len, node) = cache.match_prefix(&[1, 2, 3, 4, 5, 9, 9]);
assert_eq!(len, 5);
let n = cache.node(node);
assert_eq!(n.key, cache.page_hashes(&[1, 2, 3, 4, 5]));
assert_eq!(*n.key, cache.page_hashes(&[1, 2, 3, 4, 5]));
assert_eq!(
n.value,
*n.value,
vec![
KvPageId(10),
KvPageId(20),
Expand All @@ -1253,7 +1284,7 @@ mod tests {
let suffix_key = cache.page_hashes(&[6])[0];
let &suffix_id = n.children.get(&suffix_key).unwrap();
assert_eq!(
cache.node(suffix_id).value,
*cache.node(suffix_id).value,
vec![KvPageId(60), KvPageId(70)]
);
}
Expand Down Expand Up @@ -1286,15 +1317,31 @@ mod tests {
assert_eq!(cache.match_prefix(&[1, 2, 3, 4, 5, 6, 7, 8]).0, 8);
}

#[test]
fn repeated_prefix_splits_keep_retained_capacity_proportional_to_live_pages() {
#[rstest::rstest]
fn repeated_prefix_splits_keep_retained_capacity_proportional_to_live_pages(
#[values(false, true)] shared: bool,
) {
let mut cache = RadixCache::new(512, 1);
let tokens: Vec<u32> = (0..512).collect();
let indices: Vec<usize> = (0..512).collect();
cache.insert(&tokens, &indices);

for prefix_len in (1..512).step_by(4) {
let snapshot = shared.then(|| cache.admission_checkpoint());
let saved_edges = snapshot.as_ref().map(|saved| {
saved
.nodes
.iter()
.map(|(id, node)| (id, (node.key.to_vec(), node.value.to_vec())))
.collect::<FxHashMap<_, _>>()
});
assert_eq!(cache.match_prefix(&tokens[..prefix_len]).0, prefix_len);
if let Some(saved) = snapshot {
for (id, (key, pages)) in saved_edges.unwrap() {
assert_eq!(*saved.node(id).key, key);
assert_eq!(*saved.node(id).value, pages);
}
}
}
assert_eq!(cache.match_prefix(&tokens).0, tokens.len());

Expand Down Expand Up @@ -1331,7 +1378,7 @@ mod tests {
assert_eq!(extended, tail);
assert_eq!(cache.num_nodes(), nodes_before);
assert_eq!(
cache.node(tail).key,
*cache.node(tail).key,
cache.page_hashes(&[1, 2, 3, 4, 5, 6, 7, 8])
);
assert_eq!(cache.protected_size, 8);
Expand All @@ -1356,8 +1403,8 @@ mod tests {

assert_ne!(extended, tail);
assert_eq!(cache.num_nodes(), nodes_before + 1);
assert_eq!(cache.node(tail).key, cache.page_hashes(&[1, 2, 3, 4]));
assert_eq!(cache.node(extended).key, cache.page_hashes(&[5, 6, 7, 8]));
assert_eq!(*cache.node(tail).key, cache.page_hashes(&[1, 2, 3, 4]));
assert_eq!(*cache.node(extended).key, cache.page_hashes(&[5, 6, 7, 8]));
}

#[test]
Expand All @@ -1368,7 +1415,7 @@ mod tests {
cache.insert(&[1, 2, 3, 4, 5, 6, 7], &[0, 1, 2, 3, 4, 5, 6]);
assert_eq!(cache.match_prefix(&[1, 2, 3, 4]).0, 4);
let (_, node) = cache.match_prefix(&[1, 2, 3, 4]);
assert_eq!(cache.node(node).value, vec![KvPageId(0)]);
assert_eq!(*cache.node(node).value, vec![KvPageId(0)]);

cache.insert(&[1, 2, 3, 4, 5, 6, 7, 8], &[0, 1, 2, 3, 4, 5, 6, 7]);
assert_eq!(cache.match_prefix(&[1, 2, 3, 4, 5, 6, 7, 8]).0, 8);
Expand All @@ -1386,7 +1433,7 @@ mod tests {
cache.insert(&[1, 2, 3, 4, 5, 6, 7, 8], &[0, 1, 2, 3, 4, 5, 6, 7]);
cache.match_prefix(&[1, 2, 3, 4, 9, 9, 9, 9]);
let (_, node) = cache.match_prefix(&[1, 2, 3, 4]);
assert_eq!(cache.node(node).value, vec![KvPageId(0)]);
assert_eq!(*cache.node(node).value, vec![KvPageId(0)]);
}

#[test]
Expand All @@ -1398,7 +1445,7 @@ mod tests {

assert_eq!(cache.node(node).key.len(), 2);
assert_eq!(cache.node(node).value.len(), 2);
assert_eq!(cache.node(node).value, vec![KvPageId(0), KvPageId(1)]);
assert_eq!(*cache.node(node).value, vec![KvPageId(0), KvPageId(1)]);
assert_eq!(cache.match_prefix(&tokens).0, tokens.len());
}

Expand Down Expand Up @@ -1511,22 +1558,34 @@ mod tests {
assert_eq!(cache.match_prefix(&[4, 5, 6]).0, 3);
}

#[test]
fn partial_eviction_and_parent_promotion_keep_index_consistent() {
#[rstest::rstest]
fn partial_eviction_and_parent_promotion_keep_index_consistent(
#[values(false, true)] shared: bool,
) {
let mut cache = RadixCache::new(100, 1);
let base = Instant::now();
cache.set_test_now(base);
cache.insert(&[1, 2, 3, 4], &[0, 1, 2, 3]);
let (_, leaf) = cache.match_prefix(&[1, 2, 3, 4]);
cache.set_test_now(base + std::time::Duration::from_secs(1));
cache.insert(&[5, 6, 7], &[4, 5, 6]);
let snapshot = shared.then(|| cache.admission_checkpoint());

assert_eq!(cache.evict(2).0, 2);
assert_eq!(cache.node(leaf).key.len(), 2);
assert!(cache.evictable_leaf_is_indexed(leaf));
assert_eq!(cache.evict(2).0, 2);
assert_eq!(cache.match_prefix(&[1, 2, 3, 4]).0, 0);
assert_eq!(cache.match_prefix(&[5, 6, 7]).0, 3);
if let Some(mut saved) = snapshot {
assert_eq!(*saved.node(leaf).key, saved.page_hashes(&[1, 2, 3, 4]));
assert_eq!(
*saved.node(leaf).value,
vec![KvPageId(0), KvPageId(1), KvPageId(2), KvPageId(3)]
);
assert!(saved.evictable_leaf_is_indexed(leaf));
assert_eq!(saved.evict(2).1, vec![KvPageId(2), KvPageId(3)]);
}

let mut branched = RadixCache::new(100, 1);
branched.set_test_now(base);
Expand Down
Loading
Loading