use std::sync::mpsc;
use std::sync::Mutex;
use ferrox_core::cache::{PageGroup, SharedPagedKv};
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 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);
}