Add property-based testing for RadixAllocator (#3068)

This commit is contained in:
Daniël de Kok 2025-03-04 15:09:46 +01:00 committed by GitHub
parent fa4e9511f8
commit e88f6f6ee9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 140 additions and 0 deletions

1
Cargo.lock generated
View File

@ -4847,6 +4847,7 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"reqwest 0.11.27", "reqwest 0.11.27",
"rustc-hash 2.1.1",
"serde", "serde",
"serde_json", "serde_json",
"slotmap", "slotmap",

View File

@ -71,6 +71,7 @@ prost-build = "0.12.1"
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
itertools = "0.13" itertools = "0.13"
rustc-hash = "2"
[features] [features]
default = ["ngrok"] default = ["ngrok"]

View File

@ -635,6 +635,12 @@ fn shared_prefix(left: &[u32], right: &[u32], block_size: usize) -> usize {
mod tests { mod tests {
use std::sync::Arc; use std::sync::Arc;
use rand::{
distributions::Uniform, prelude::Distribution, rngs::SmallRng, seq::SliceRandom,
SeedableRng,
};
use rustc_hash::FxHashSet;
use super::*; use super::*;
#[test] #[test]
@ -900,4 +906,136 @@ mod tests {
assert_eq!(blocks, vec![0, 1]); assert_eq!(blocks, vec![0, 1]);
assert_eq!(node_id, trie.find(&[0, 1], &mut blocks)) assert_eq!(node_id, trie.find(&[0, 1], &mut blocks))
} }
struct AllocationWithInfo {
allocation: BlockAllocation,
// We are doing a lot of set operations and `FxBuildHasher` is
// muc faster for a set of integers.
blockset: FxHashSet<u32>,
non_prefix_blocks: FxHashSet<u32>,
}
#[test]
fn invariants_hold_on_many_operations_remove_all() {
invariants_hold_on_many_insertions(true);
}
#[test]
fn invariants_hold_on_many_operations_remove_subset() {
invariants_hold_on_many_insertions(false);
}
fn invariants_hold_on_many_insertions(remove_all: bool) {
// Small vocabulary sizes lead to violations more quickly due to
// prefix sharing, etc.
const VOCAB_SIZE: u32 = 2;
const DATA_LEN: usize = 1_000;
const MAX_PREFILL_LEN: usize = 8;
const MAX_DECODE_LEN: usize = 8;
let vocab_range = Uniform::new(0, VOCAB_SIZE);
let data_range = Uniform::new(0, DATA_LEN);
let prefill_len_range = Uniform::new(0, MAX_PREFILL_LEN);
let decode_len_range = Uniform::new(0, MAX_DECODE_LEN);
let mut rng = SmallRng::seed_from_u64(64);
let data = (0..DATA_LEN)
.map(|_| vocab_range.sample(&mut rng))
.collect::<Vec<_>>();
let mut allocator = RadixAllocator::new(1, 100, None);
let mut allocations = Vec::new();
for i in 0..100_000 {
// Allocate until all blocks are used.
'allocation: loop {
// Use offset 0 half of the times for prefix sharing.
let prefill_offset = data_range.sample(&mut rng);
let prefill_len = prefill_len_range.sample(&mut rng);
let decode_len = decode_len_range.sample(&mut rng);
let prefill =
data[prefill_offset..data.len().min(prefill_offset + prefill_len)].to_vec();
let allocation = match allocator
.allocate((prefill.len() + decode_len) as u32, Some(Arc::new(prefill)))
{
Some(allocation) => allocation,
None => break 'allocation,
};
let non_prefix_blocks = allocation.blocks[allocation.prefix_len as usize..]
.iter()
.copied()
.collect::<FxHashSet<_>>();
let blockset = allocation.blocks.iter().copied().collect::<FxHashSet<_>>();
// No duplicate blocks in an allocation.
assert_eq!(
allocation.blocks.len(),
blockset.len(),
"Duplicate blocks in allocation"
);
allocations.push(AllocationWithInfo {
allocation,
blockset,
non_prefix_blocks,
});
}
// Check invariants. Skip first iteration, since there is no prefix sharing yet.
if i > 1 {
check_allocation_invariants(&allocations);
}
// Remove 20% of the allocations, randomly.
if remove_all {
allocations.into_iter().for_each(|allocation| {
allocator.free(
allocation.allocation.blocks.clone(),
allocation.allocation.allocation_id,
)
});
allocations = Vec::new();
} else {
allocations.shuffle(&mut rng);
let remove_index = (allocations.len() as f64 * 0.8) as usize;
for allocation in allocations.drain(remove_index..) {
allocator.free(
allocation.allocation.blocks.clone(),
allocation.allocation.allocation_id,
);
}
}
}
}
fn check_allocation_invariants(allocations: &[AllocationWithInfo]) {
for i in 0..allocations.len() {
let allocation = &allocations[i];
// 0 is used for health checks, must not be used.
assert!(
!allocation.blockset.contains(&0),
"Block 0 must not be allocated"
);
// No duplicate blocks in an allocation.
assert_eq!(
allocation.allocation.blocks.len(),
allocation.blockset.len(),
"Duplicate blocks in allocation"
);
for other_allocation in &allocations[i + 1..] {
assert!(
other_allocation
.non_prefix_blocks
.is_disjoint(&allocation.non_prefix_blocks),
"Allocations share non-prefix blocks"
)
}
}
}
} }