use std::sync::mpsc;
use std::sync::Mutex;
use ferrox_core::cache::{PageGroup, SharedPagedKv};
use crate::generate::acquire_paged_caches;
use crate::policy::radix::RadixCache;
use super::super::row::{Job, RowKv, Rows};
use super::super::worker::accept;
use super::*;
const BLOCK: usize = 4;
fn paged_with_radix(
decoder: &Arc<Decoder>,
groups: usize,
) -> (PagedKvConfig, Arc<SharedPagedKv>, Arc<Mutex<RadixCache>>) {
let radix = Arc::new(Mutex::new(RadixCache::new(BLOCK)));
let store = Arc::new(SharedPagedKv::new(
decoder.layers.len(),
BLOCK,
groups,
decoder.config.n_kv_heads,
decoder.config.head_dim,
));
let config = PagedKvConfig {
store: Arc::clone(&store),
queue_wait: std::time::Duration::ZERO,
radix: Some(Arc::clone(&radix)),
anchor_token: None,
slide_interval: crate::policy::pool_budget::DEFAULT_SWA_EVICTION_INTERVAL,
};
(config, store, radix)
}
fn paged_job(prompt: Vec<usize>, max_tokens: usize) -> (Job, mpsc::Receiver<BatcherEvent>) {
let (tx, rx) = mpsc::channel();
(
Job {
prompt_tokens: prompt,
params: greedy_params(max_tokens, 7),
stop_tokens: StopTokens::default(),
reply: tx,
abort: AbortId(0),
blocks: 1,
},
rx,
)
}
fn admit_prefilled(
decoder: &Arc<Decoder>,
config: &PagedKvConfig,
prompt: Vec<usize>,
max_tokens: usize,
) -> Option<(Slot, mpsc::Receiver<BatcherEvent>)> {
let (job, rx) = paged_job(prompt, max_tokens);
let mut prefill = accept(decoder, job, 1, Some(config))?;
while !prefill.state.step_chunk() {}
Some((prefill.into_slot(), rx))
}
fn finish_row(slot: Slot) {
let mut rows = Rows::default();
let uid = rows.insert(slot);
rows.get_mut(uid).expect("just inserted").finish = Some(FinishReason::Length);
rows.flush_finished(&no_budget());
}
fn published_groups(radix: &Arc<Mutex<RadixCache>>, prompt: &[usize]) -> (usize, Vec<u32>) {
let ids: Vec<u32> = prompt.iter().map(|&t| t as u32).collect();
let mut tree = radix.lock().unwrap();
let m = tree.match_prefix(&ids);
if m.cached_len == 0 {
return (0, Vec::new());
}
let per_token = tree.matched_indices(m.node);
(
m.cached_len,
per_token[..m.cached_len]
.iter()
.copied()
.step_by(BLOCK)
.collect(),
)
}
#[test]
fn a_finished_batched_row_publishes_its_prefix() {
let decoder = tiny_decoder();
let (config, _store, radix) = paged_with_radix(&decoder, 64);
assert_eq!(
radix.lock().unwrap().total_size(),
0,
"the tree starts empty"
);
let prompt = vec![1usize, 2, 3, 4, 5, 6, 7, 8];
let (slot, _rx) = admit_prefilled(&decoder, &config, prompt.clone(), 4).expect("admitted");
finish_row(slot);
let (cached, groups) = published_groups(&radix, &prompt);
assert_eq!(
cached,
prompt.len(),
"a finished paged row must leave its whole prefix in the tree"
);
assert_eq!(groups.len(), prompt.len() / BLOCK);
}
#[test]
fn a_second_batched_request_adopts_the_pages_the_first_published() {
let decoder = tiny_decoder();
let (config, store, radix) = paged_with_radix(&decoder, 64);
let prompt = vec![1usize, 2, 3, 4, 5, 6, 7, 8];
let (first, _rx1) = admit_prefilled(&decoder, &config, prompt.clone(), 4).expect("admitted");
finish_row(first);
let (cached, published) = published_groups(&radix, &prompt);
assert_eq!(cached, prompt.len(), "the first row must have published");
for &g in &published {
assert_eq!(
store.group_refs(PageGroup(g)),
1,
"with no row alive, only the tree holds a published page"
);
}
let (second, _rx2) = admit_prefilled(&decoder, &config, prompt.clone(), 4).expect("admitted");
let RowKv::Paged(lease) = &second.kv else {
panic!("a paged config must produce a paged row");
};
let adopted = lease.adopted_positions(BLOCK);
assert!(
adopted > 0,
"the second request must adopt the prefix the first published, \
got {adopted} adopted positions"
);
for &g in published.iter().take(adopted / BLOCK) {
assert_eq!(
store.group_refs(PageGroup(g)),
2,
"an adopted page is held by the tree AND by the row reading it"
);
}
drop(second);
for &g in &published {
assert_eq!(
store.group_refs(PageGroup(g)),
1,
"the row's hold goes back on Drop; the tree's survives"
);
}
}
#[test]
fn a_batched_prefill_over_an_adopted_prefix_ends_exactly_at_the_prompt_length() {
let decoder = tiny_decoder();
let (config, _store, _radix) = paged_with_radix(&decoder, 64);
let prompt = vec![1usize, 2, 3, 4, 5, 6, 7, 8];
let (first, _rx1) = admit_prefilled(&decoder, &config, prompt.clone(), 4).expect("admitted");
finish_row(first);
let (job, _rx2) = paged_job(prompt.clone(), 4);
let mut prefill = accept(&decoder, job, 3, Some(&config)).expect("admitted");
let adopted = prefill.state.tokens_processed();
assert_eq!(
adopted, BLOCK,
"the second request must start where the prefix it adopted ends"
);
assert_eq!(prefill.state.tokens_remaining(), prompt.len() - BLOCK);
while !prefill.state.step_chunk() {}
let mut slot = prefill.into_slot();
assert_eq!(
slot.pos,
prompt.len(),
"the first generated token sits at the end of the prompt"
);
let RowKv::Paged(lease) = &mut slot.kv else {
panic!("a paged config must produce a paged row");
};
for (layer, cache) in lease.caches_mut().iter().enumerate() {
assert_eq!(
cache.seq_len(),
prompt.len(),
"layer {layer}: a warm prefill left {} KV rows for a {}-token \
prompt, so it re-ran the prompt on top of the adopted prefix",
cache.seq_len(),
prompt.len()
);
}
}
#[test]
fn a_warm_batched_request_produces_the_same_logits_as_a_cold_one() {
let decoder = tiny_decoder();
let (config, _store, _radix) = paged_with_radix(&decoder, 64);
let prompt = vec![1usize, 2, 3, 4, 5, 6, 7, 8];
let cold = {
let (job, _rx) = paged_job(prompt.clone(), 4);
let mut prefill = accept(&decoder, job, 3, Some(&config)).expect("admitted");
assert_eq!(
prefill.state.tokens_processed(),
0,
"the first request has nothing to adopt"
);
while !prefill.state.step_chunk() {}
let (_kv, logits, _pos, _ids) = prefill.state.into_decode_start();
logits
};
let (first, _rx1) = admit_prefilled(&decoder, &config, prompt.clone(), 4).expect("admitted");
finish_row(first);
let (job, _rx2) = paged_job(prompt.clone(), 4);
let mut prefill = accept(&decoder, job, 3, Some(&config)).expect("admitted");
assert!(
prefill.state.tokens_processed() > 0,
"this test proves nothing unless the second request adopted a prefix"
);
while !prefill.state.step_chunk() {}
let (_kv, warm, _pos, _ids) = prefill.state.into_decode_start();
assert_eq!(
warm, cold,
"a prefix-cache hit changed the answer: the warm request's logits \
differ from the cold request's for the same prompt"
);
}
#[test]
fn the_leases_adopted_count_and_its_seeded_kv_rows_agree() {
let decoder = tiny_decoder();
let (config, _store, _radix) = paged_with_radix(&decoder, 64);
let prompt = vec![1usize, 2, 3, 4, 5, 6, 7, 8];
let (first, _rx1) = admit_prefilled(&decoder, &config, prompt.clone(), 4).expect("admitted");
finish_row(first);
let mut lease = acquire_paged_caches(&decoder, &config, &prompt, prompt.len() + 4)
.expect("the store has pages to spare");
let claimed = lease.adopted_positions(BLOCK);
assert!(claimed > 0, "the second request must adopt something");
for (layer, cache) in lease.caches_mut().iter().enumerate() {
assert_eq!(
cache.seq_len(),
claimed,
"layer {layer}: the lease says {claimed} positions were adopted \
but its KV was seeded with {} rows",
cache.seq_len()
);
}
}
#[test]
fn batched_publishing_does_not_starve_the_page_pool() {
let decoder = tiny_decoder();
let (config, store, radix) = paged_with_radix(&decoder, 8);
let free_at_start = store.free_groups();
assert_eq!(free_at_start, 8);
for cycle in 0..12u32 {
let prompt: Vec<usize> = std::iter::once(10 + cycle as usize % 20)
.chain([1, 2, 3, 4, 5, 6, 7])
.collect();
let (slot, _rx) = admit_prefilled(&decoder, &config, prompt, 4).unwrap_or_else(|| {
panic!(
"cycle {cycle} was refused: the tree is holding pages it will \
not give back, so the pool shrank monotonically \
(free = {}, tree = {})",
store.free_groups(),
radix.lock().unwrap().total_size()
)
});
finish_row(slot);
}
assert!(
store.free_groups() > 0,
"every page ended up parked in the tree"
);
assert!(store.free_groups() <= free_at_start);
}