use super::*;
fn reference_gains(
docs: &[u32],
fwd: &ForwardIndex,
mid: usize,
degrees: &TermDegrees,
log_table: &[f32],
) -> Vec<f32> {
docs.iter()
.enumerate()
.map(|(i, &doc)| {
let in_left = i < mid;
let mut gain = 0.0f32;
for &term in fwd.doc_terms(doc as usize) {
let [left, right] = degrees.get(term as usize);
let (from, to) = if in_left {
(left, right)
} else {
(right, left)
};
let movement = fast_log2_lookup(to as usize + 2, log_table)
- fast_log2_lookup(from as usize, log_table)
- std::f32::consts::LOG2_E / (1.0 + to as f32);
gain += if in_left { movement } else { -movement };
}
gain
})
.collect()
}
fn fixture(n: usize, vocabulary: usize, width: usize, lanes: usize) -> ForwardIndex {
let mut terms = Vec::with_capacity(n * width);
let mut offsets = Vec::with_capacity(n + 1);
offsets.push(0);
let mut random = 0xc0ffee1234567890u64;
let mut row = Vec::with_capacity(width);
for doc in 0..n {
if doc % 97 != 0 {
row.clear();
let topic = (doc * 7919) % 64;
for i in 0..width {
random ^= random << 13;
random ^= random >> 7;
random ^= random << 17;
let term = if i % 2 == 0 {
random as usize % (vocabulary / 16).max(1)
} else {
(topic * (vocabulary / 64) + random as usize % 64) % vocabulary
};
row.push(term as u32);
}
row.sort_unstable();
row.dedup();
terms.extend_from_slice(&row);
}
offsets.push(terms.len() as u64);
}
ForwardIndex {
terms,
offsets,
num_terms: vocabulary,
parallel_bisect_lanes: lanes,
cache_gains: true,
budget_limited: false,
}
}
#[test]
fn gain_bits_preserve_half_direction_empty_rows_and_reused_degrees() {
let fwd = fixture(10_003, 257, 24, 1);
let mut docs: Vec<u32> = (0..fwd.num_docs() as u32).collect();
let log_table = build_log_table(4096);
let mut workspaces = [TermDegrees::new(fwd.num_terms)];
let mut gains = vec![0.0; docs.len()];
for mid in [1, 5001, 10_002] {
docs.rotate_left(31);
build_term_degrees(&docs, mid, &fwd, &mut workspaces, None);
let expected = reference_gains(&docs, &fwd, mid, &workspaces[0], &log_table);
for cached in [false, true] {
if cached {
workspaces[0]
.gain_cache
.resize(fwd.num_terms, [f32::NAN; 2]);
} else {
workspaces[0].gain_cache.clear();
}
for parallel in [false, true] {
compute_gains(
&docs,
&fwd,
mid,
&mut workspaces[0],
&log_table,
&mut gains,
parallel,
);
assert_eq!(
gains.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
expected.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"mid={mid}, parallel={parallel}, cached={cached}",
);
}
}
}
}
#[test]
fn gain_cache_requires_spare_memory_for_every_existing_lane() {
let terms = 130;
let lanes = 3;
let fixed = 20_000;
let required = fixed
+ LOG_TABLE_SIZE * std::mem::size_of::<f32>()
+ lanes * (term_degree_bytes(terms) + terms * 8 + std::mem::size_of::<TermDegrees>());
assert!(!gain_cache_fits(required - 1, fixed, terms, lanes));
assert!(gain_cache_fits(required, fixed, terms, lanes));
assert!(!gain_cache_fits(usize::MAX, fixed, usize::MAX, lanes));
assert!(!gain_cache_fits(usize::MAX, fixed, terms, usize::MAX));
assert!(!gain_cache_fits(usize::MAX, fixed, 0, lanes));
}
#[test]
fn cached_gains_preserve_full_permutations_and_reuse_lane_storage() {
let mut fwd = fixture(2051, 1024, 24, 1);
fwd.cache_gains = false;
let direct = graph_bisection(&fwd, 32, 12, BpBudget::full());
fwd.cache_gains = true;
assert_eq!(graph_bisection(&fwd, 32, 12, BpBudget::full()), direct);
let docs: Vec<u32> = (0..fwd.num_docs() as u32).collect();
let mut degrees = [TermDegrees::new(fwd.num_terms)];
degrees[0].gain_cache = vec![[f32::NAN; 2]; fwd.num_terms];
let cache_ptr = degrees[0].gain_cache.as_ptr();
let logs = build_log_table(4096);
for subset in [&docs[..], &docs[10..43], &docs[900..911]] {
let mid = subset.len() / 2;
build_term_degrees(subset, mid, &fwd, &mut degrees, None);
let expected = reference_gains(subset, &fwd, mid, °rees[0], &logs);
let mut actual = vec![0.0; subset.len()];
compute_gains(
subset,
&fwd,
mid,
&mut degrees[0],
&logs,
&mut actual,
false,
);
assert_eq!(
actual.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
expected.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
);
assert_eq!(degrees[0].gain_cache.as_ptr(), cache_ptr);
}
}
#[cfg(feature = "native")]
#[test]
#[ignore]
fn bench_bp_gain_review() {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(4)
.build()
.unwrap();
let output = std::env::var_os("HERMES_BP_EVIDENCE_DIR").map(std::path::PathBuf::from);
if let Some(path) = &output {
std::fs::create_dir_all(path).unwrap();
}
for (name, n, vocabulary, width, min_partition) in [
("mixed", 100_003, 16_384, 48, 32),
("sparse", 100_003, 131_072, 4, 32),
("coarse", 1_048_577, 1024, 12, 524_288),
] {
let fwd = fixture(n, vocabulary, width, 4);
let mut expected = None;
let mut times = Vec::new();
for round in 0..4 {
let started = std::time::Instant::now();
let (order, converged) = pool.install(|| {
graph_bisection_with_progress(
&fwd,
min_partition,
12,
BpBudget::full(),
None,
BpProgressLabel {
index: "review",
field: name,
entity_kind: "records",
},
)
});
let elapsed = started.elapsed();
assert!(converged);
if round > 0 {
times.push(elapsed.as_secs_f64() * 1000.0);
}
if let Some(prior) = &expected {
assert_eq!(&order, prior);
} else {
if let Some(path) = &output {
let bytes: Vec<_> = order.iter().flat_map(|id| id.to_le_bytes()).collect();
std::fs::write(path.join(format!("{name}.bin")), bytes).unwrap();
}
let mut sorted = order.clone();
sorted.sort_unstable();
assert!(sorted.iter().enumerate().all(|(i, &id)| i == id as usize));
expected = Some(order);
}
}
times.sort_by(f64::total_cmp);
println!(
"BP {name}: docs={n} postings={} terms={vocabulary} lanes=4 median_ms={:.3} rounds_ms={times:?} degree_bytes_per_lane={} gain_cache_bytes={}",
fwd.total_postings(),
times[1],
term_degree_bytes(vocabulary),
vocabulary * 8 * 4,
);
}
}