Migrate InvertedIndexCompressedMmap to UIO (#9144)

* Migrate InvertedIndexCompressedMmap to UniversalRead

* Rehaul search_scratch.rs (was scores_memory_pool.rs)

- Use `typed_arena::Arena` instead of `bumpalo::Bump`.
  Reason: bump don't drop.
  Drawback: `Arena::new()` is not free, and `Arena::clear()` doesn't
  exist; but this commit has workarounds.
- Use names that make more sense.

* Misc fixes

* InvertedIndexCompressedMmap: explicit S type parameter
This commit is contained in:
xzfc
2026-05-29 17:49:49 +00:00
committed by timvisee
parent 623063282a
commit 779a66f783
24 changed files with 444 additions and 296 deletions

7
Cargo.lock generated
View File

@@ -7530,6 +7530,7 @@ dependencies = [
"sha2 0.11.0",
"sparse",
"tempfile",
"typed-arena",
"validator",
"zerocopy",
]
@@ -8382,6 +8383,12 @@ dependencies = [
"rand 0.9.2",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typeid"
version = "1.0.3"

View File

@@ -1,7 +1,8 @@
use std::borrow::Cow;
use std::ops::Deref;
use aligned_vec::{AVec, Alignment, RuntimeAlign};
use aligned_vec::Alignment;
pub use aligned_vec::{AVec, RuntimeAlign};
use bytemuck::PodCastError;
#[derive(Debug)]

View File

@@ -2,6 +2,7 @@ use std::sync::atomic::AtomicBool;
use common::counter::hardware_counter::HardwareCounterCell;
use common::types::PointOffsetType;
use common::universal_io::MmapFile;
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use dataset::Dataset;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
@@ -104,7 +105,7 @@ fn sparse_vector_index_search_benchmark_impl(
let sparse_index_config =
SparseIndexConfig::new(Some(FULL_SCAN_THRESHOLD), SparseIndexType::Mmap, None);
let pb = progress("Indexing (2/2)", vectors_len);
let sparse_vector_index_mmap: SparseVectorIndex<InvertedIndexCompressedMmap<f32>> =
let sparse_vector_index_mmap: SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>> =
SparseVectorIndex::open(SparseVectorIndexOpenArgs {
config: sparse_index_config,
id_tracker: sparse_vector_index.id_tracker().clone(),

View File

@@ -118,9 +118,9 @@ macro_rules! fixture_for_all_indices {
::sparse::index::inverted_index::inverted_index_compressed_immutable_ram::InvertedIndexCompressedImmutableRam<f32>
>($($args)*);
eprintln!("InvertedIndexCompressedMmap<f32>");
eprintln!("InvertedIndexCompressedMmap<f32, MmapFile>");
$test::<
::sparse::index::inverted_index::inverted_index_compressed_mmap::InvertedIndexCompressedMmap<f32>
::sparse::index::inverted_index::inverted_index_compressed_mmap::InvertedIndexCompressedMmap<f32, MmapFile>
>($($args)*);
};
}

View File

@@ -9,7 +9,7 @@ use common::counter::hardware_counter::HardwareCounterCell;
use common::generic_consts::Random;
use common::storage_version::StorageVersion as _;
use fs_err as fs;
use sparse::common::scores_memory_pool::ScoresMemoryPool;
use sparse::SearchScratchPool;
use sparse::common::sparse_vector::SparseVector;
use sparse::index::inverted_index::InvertedIndex;
use sparse::index::inverted_index::inverted_index_ram_builder::InvertedIndexBuilder;
@@ -35,7 +35,7 @@ pub struct SparseVectorIndex<TInvertedIndex: InvertedIndex> {
inverted_index: TInvertedIndex,
searches_telemetry: SparseSearchesTelemetry,
indices_tracker: IndicesTracker,
scores_memory_pool: ScoresMemoryPool,
search_scratch_pool: SearchScratchPool,
}
/// Getters for internals, used for testing.
@@ -130,7 +130,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
let searches_telemetry = SparseSearchesTelemetry::new();
let path = path.to_path_buf();
let scores_memory_pool = ScoresMemoryPool::new();
let search_scratch_pool = SearchScratchPool::new();
Ok(Self {
config,
id_tracker,
@@ -140,7 +140,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
inverted_index,
searches_telemetry,
indices_tracker,
scores_memory_pool,
search_scratch_pool,
})
}
@@ -227,12 +227,14 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
let hw_counter = HardwareCounterCell::disposable();
let mut unique_record_ids = std::collections::HashSet::new();
let mut arena = sparse::SearchScratchArena::new_slow();
for dim_id in &query_vector.indices {
if let Some(dim_id) = self.indices_tracker.remap_index(*dim_id) {
let posting_list_iter = self.inverted_index.get(dim_id, &hw_counter)?;
let posting_list_iter = self.inverted_index.get(dim_id, &arena, &hw_counter)?;
for element in posting_list_iter.into_std_iter() {
unique_record_ids.insert(element.record_id);
}
arena.gc();
}
}
Ok(unique_record_ids.len())

View File

@@ -132,7 +132,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
.collect_vec();
let sparse_vector = self.indices_tracker.remap_vector(sparse_vector.clone());
let memory_handle = self.scores_memory_pool.get();
let mut scratch = self.search_scratch_pool.get();
let mut hw_counter = vector_query_context.hardware_counter();
let is_index_on_disk = self.config.index_type.is_on_disk();
if is_index_on_disk {
@@ -145,7 +145,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
sparse_vector,
top,
&self.inverted_index,
memory_handle,
&mut scratch,
&is_stopped,
&hw_counter,
)?;
@@ -175,7 +175,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
let is_stopped = vector_query_context.is_stopped();
let sparse_vector = self.indices_tracker.remap_vector(sparse_vector.clone());
let memory_handle = self.scores_memory_pool.get();
let mut scratch = self.search_scratch_pool.get();
let mut hw_counter = vector_query_context.hardware_counter();
let is_index_on_disk = self.config.index_type.is_on_disk();
if is_index_on_disk {
@@ -188,7 +188,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
sparse_vector,
top,
&self.inverted_index,
memory_handle,
&mut scratch,
&is_stopped,
&hw_counter,
)?;

View File

@@ -3,6 +3,7 @@ use std::path::PathBuf;
use common::counter::hardware_counter::HardwareCounterCell;
use common::types::{PointOffsetType, ScoredPointOffset, TelemetryDetail};
use common::universal_io::MmapFile;
use half::f16;
use sparse::common::types::{DimId, QuantizedU8};
use sparse::index::inverted_index::InvertedIndex;
@@ -95,9 +96,9 @@ pub enum VectorIndexEnum {
SparseCompressedImmutableRamU8(
SparseVectorIndex<InvertedIndexCompressedImmutableRam<QuantizedU8>>,
),
SparseCompressedMmapF32(SparseVectorIndex<InvertedIndexCompressedMmap<f32>>),
SparseCompressedMmapF16(SparseVectorIndex<InvertedIndexCompressedMmap<f16>>),
SparseCompressedMmapU8(SparseVectorIndex<InvertedIndexCompressedMmap<QuantizedU8>>),
SparseCompressedMmapF32(SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>>),
SparseCompressedMmapF16(SparseVectorIndex<InvertedIndexCompressedMmap<f16, MmapFile>>),
SparseCompressedMmapU8(SparseVectorIndex<InvertedIndexCompressedMmap<QuantizedU8, MmapFile>>),
}
impl VectorIndexEnum {

View File

@@ -6,6 +6,7 @@ use common::counter::hardware_counter::HardwareCounterCell;
use common::generic_consts::Random;
use common::storage_version::VERSION_FILE;
use common::types::{PointOffsetType, TelemetryDetail};
use common::universal_io::MmapFile;
use fs_err as fs;
use itertools::Itertools;
use rand::SeedableRng;
@@ -170,9 +171,10 @@ fn check_index_storage_consistency<T: InvertedIndex>(sparse_vector_index: &Spars
.iter()
.zip(remapped_vector.values.iter())
{
let arena = sparse::SearchScratchArena::new_slow();
let posting_list = sparse_vector_index
.inverted_index()
.get(*dim_id, &hw_counter)
.get(*dim_id, &arena, &hw_counter)
.unwrap();
// assert posting list sorted by record id
assert!(
@@ -221,7 +223,7 @@ fn sparse_vector_index_consistent_with_storage() {
// create mmap sparse vector index
let mut sparse_index_config = sparse_vector_ram_index.config();
sparse_index_config.index_type = SparseIndexType::Mmap;
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32>> =
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>> =
SparseVectorIndex::open(SparseVectorIndexOpenArgs {
config: sparse_index_config,
id_tracker: sparse_vector_ram_index.id_tracker().clone(),
@@ -247,7 +249,7 @@ fn sparse_vector_index_consistent_with_storage() {
// load index from memmap file
let mut sparse_index_config = sparse_vector_ram_index.config();
sparse_index_config.index_type = SparseIndexType::Mmap;
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32>> =
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>> =
SparseVectorIndex::open(SparseVectorIndexOpenArgs {
config: sparse_index_config,
id_tracker: sparse_vector_ram_index.id_tracker().clone(),
@@ -271,13 +273,14 @@ fn sparse_vector_index_consistent_with_storage() {
#[test]
fn sparse_vector_index_load_missing_mmap() {
let data_dir = Builder::new().prefix("data_dir").tempdir().unwrap();
let sparse_vector_index: OperationResult<SparseVectorIndex<InvertedIndexCompressedMmap<f32>>> =
fixture_sparse_index_from_iter(
data_dir.path(),
[].iter().cloned(),
10_000,
SparseIndexType::Mmap,
);
let sparse_vector_index: OperationResult<
SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>>,
> = fixture_sparse_index_from_iter(
data_dir.path(),
[].iter().cloned(),
10_000,
SparseIndexType::Mmap,
);
// absent configuration file for mmap are ignored
// a new index is created
assert!(sparse_vector_index.is_ok())

View File

@@ -33,6 +33,7 @@ itertools = { workspace = true }
parking_lot = { workspace = true }
log = { workspace = true }
zerocopy = { workspace = true }
typed-arena = "2.0.2"
[dev-dependencies]
criterion = { workspace = true }

View File

@@ -4,6 +4,7 @@ use std::sync::atomic::AtomicBool;
use common::counter::hardware_counter::HardwareCounterCell;
use common::types::PointOffsetType;
use common::universal_io::MmapFile;
use criterion::measurement::Measurement;
use criterion::{Criterion, criterion_group, criterion_main};
use dataset::Dataset;
@@ -13,7 +14,7 @@ use itertools::Itertools;
use rand::SeedableRng as _;
use rand::rngs::StdRng;
use sha2::Digest;
use sparse::common::scores_memory_pool::ScoresMemoryPool;
use sparse::SearchScratchPool;
use sparse::common::sparse_vector::{RemappedSparseVector, SparseVector};
use sparse::common::sparse_vector_fixture::{random_positive_sparse_vector, random_sparse_vector};
use sparse::common::types::{QuantizedU8, Weight};
@@ -162,7 +163,7 @@ pub fn run_bench(
run_bench2(
c.benchmark_group(format!("search/mmap_{}/{name}", $name)),
&InvertedIndexCompressedMmap::<$type>::open(&index_path).unwrap(),
&InvertedIndexCompressedMmap::<$type, MmapFile>::open(&index_path).unwrap(),
query_vectors,
&hottest_query_vectors,
);
@@ -181,7 +182,7 @@ fn run_bench2(
query_vectors: &[SparseVector],
hottest_query_vectors: &[RemappedSparseVector],
) {
let pool = ScoresMemoryPool::new();
let pool = SearchScratchPool::new();
let stopped = AtomicBool::new(false);
let mut it = query_vectors.iter().cycle();
@@ -192,7 +193,8 @@ fn run_bench2(
b.iter_batched(
|| it.next().unwrap().clone().into_remapped(),
|vec| {
SearchContext::new(vec, TOP, index, pool.get(), &stopped, &hardware_counter)
let mut scratch = pool.get();
SearchContext::new(vec, TOP, index, &mut scratch, &stopped, &hardware_counter)
.unwrap()
.search(&|_| true)
},
@@ -207,7 +209,8 @@ fn run_bench2(
b.iter_batched(
|| it.next().unwrap().clone(),
|vec| {
SearchContext::new(vec, TOP, index, pool.get(), &stopped, &hardware_counter)
let mut scratch = pool.get();
SearchContext::new(vec, TOP, index, &mut scratch, &stopped, &hardware_counter)
.unwrap()
.search(&|_| true)
},
@@ -267,7 +270,8 @@ fn cached_compressed_index<W: Weight>(index: &InvertedIndexRam, name: &str) -> P
fs::remove_dir_all(&tmp_path).unwrap();
}
fs::create_dir_all(&tmp_path).unwrap();
InvertedIndexCompressedMmap::<W>::from_ram_index(Cow::Borrowed(index), &tmp_path).unwrap();
InvertedIndexCompressedMmap::<W, MmapFile>::from_ram_index(Cow::Borrowed(index), &tmp_path)
.unwrap();
fs::rename(tmp_path, &path).unwrap();
}
eprintln!("Using cache: {path:?}");

View File

@@ -1,4 +1,3 @@
pub mod scores_memory_pool;
pub mod sparse_vector;
#[cfg(feature = "testing")]
pub mod sparse_vector_fixture;

View File

@@ -1,56 +0,0 @@
use common::defaults::POOL_KEEP_LIMIT;
use common::types::ScoreType;
use parking_lot::Mutex;
type PooledScores = Vec<ScoreType>;
#[derive(Debug)]
pub struct PooledScoresHandle<'a> {
pool: &'a ScoresMemoryPool,
pub scores: PooledScores,
}
impl<'a> PooledScoresHandle<'a> {
fn new(pool: &'a ScoresMemoryPool, scores: PooledScores) -> Self {
PooledScoresHandle { pool, scores }
}
}
impl Drop for PooledScoresHandle<'_> {
fn drop(&mut self) {
self.pool.return_back(std::mem::take(&mut self.scores));
}
}
#[derive(Debug)]
pub struct ScoresMemoryPool {
pool: Mutex<Vec<PooledScores>>,
}
impl ScoresMemoryPool {
pub fn new() -> Self {
ScoresMemoryPool {
pool: Mutex::new(Vec::with_capacity(*POOL_KEEP_LIMIT)),
}
}
pub fn get(&self) -> PooledScoresHandle<'_> {
match self.pool.lock().pop() {
None => PooledScoresHandle::new(self, vec![]),
Some(data) => PooledScoresHandle::new(self, data),
}
}
fn return_back(&self, data: PooledScores) {
let mut pool = self.pool.lock();
if pool.len() < *POOL_KEEP_LIMIT {
pool.push(data);
}
}
}
impl Default for ScoresMemoryPool {
fn default() -> Self {
Self::new()
}
}

View File

@@ -17,7 +17,7 @@ use crate::common::types::{DimWeight, Weight};
type BitPackerImpl = bitpacking::BitPacker4x;
/// How many elements are packed in a single chunk.
const CHUNK_SIZE: usize = BitPackerImpl::BLOCK_LEN;
pub(super) const CHUNK_SIZE: usize = BitPackerImpl::BLOCK_LEN;
#[derive(Default, Debug, Clone, PartialEq)]
pub struct CompressedPostingList<W: Weight> {

View File

@@ -3,17 +3,18 @@ use std::path::Path;
use common::counter::hardware_counter::HardwareCounterCell;
use common::types::PointOffsetType;
use common::universal_io::Result;
use common::universal_io::{MmapFs, Result};
use super::inverted_index_compressed_mmap::InvertedIndexCompressedMmap;
use super::inverted_index_ram::InvertedIndexRam;
use super::{InvertedIndex, out_of_bounds};
use crate::SearchScratchArena;
use crate::common::sparse_vector::RemappedSparseVector;
use crate::common::types::{DimId, DimOffset, Weight};
use crate::index::compressed_posting_list::{
CompressedPostingBuilder, CompressedPostingList, CompressedPostingListIterator,
CompressedPostingListView,
};
use crate::index::posting_list_common::PostingListIter as _;
#[derive(Debug, Clone, PartialEq)]
pub struct InvertedIndexCompressedImmutableRam<W: Weight> {
@@ -22,17 +23,19 @@ pub struct InvertedIndexCompressedImmutableRam<W: Weight> {
pub(super) total_sparse_size: usize,
}
type Storage = common::universal_io::MmapFile;
impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
type Iter<'a> = CompressedPostingListIterator<'a, W>;
type Version = <InvertedIndexCompressedMmap<W> as InvertedIndex>::Version;
type Version = <InvertedIndexCompressedMmap<W, Storage> as InvertedIndex>::Version;
fn is_on_disk(&self) -> bool {
false
}
fn open(path: &Path) -> Result<Self> {
let mmap_inverted_index = InvertedIndexCompressedMmap::load(path)?;
let mmap_inverted_index = InvertedIndexCompressedMmap::<W, Storage>::load(&MmapFs, path)?;
let mut inverted_index = InvertedIndexCompressedImmutableRam {
postings: Vec::with_capacity(mmap_inverted_index.file_header.posting_count),
vector_count: mmap_inverted_index.file_header.vector_count,
@@ -41,9 +44,11 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
let hw_counter = HardwareCounterCell::disposable();
let mut arena = SearchScratchArena::new_slow();
for i in 0..mmap_inverted_index.file_header.posting_count as DimId {
let posting_list = mmap_inverted_index.get(i, &hw_counter)?;
let posting_list = mmap_inverted_index.get(i, &arena, &hw_counter)?;
inverted_index.postings.push(posting_list.to_owned());
arena.gc();
}
mmap_inverted_index.clear_cache()?;
@@ -51,21 +56,18 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
Ok(inverted_index)
}
fn save(&self, path: &Path) -> std::io::Result<()> {
InvertedIndexCompressedMmap::convert_and_save(self, path)?;
fn save(&self, path: &Path) -> Result<()> {
InvertedIndexCompressedMmap::<W, Storage>::convert_and_save(&MmapFs, self, path)?;
Ok(())
}
fn get<'a>(
&'a self,
id: DimOffset,
_arena: &'a SearchScratchArena,
hw_counter: &'a HardwareCounterCell, // Ignored for in-ram index
) -> Result<Self::Iter<'a>> {
let posting = self
.postings
.get(id as usize)
.ok_or(out_of_bounds(id, self.len()))?;
Ok(posting.iter(hw_counter))
Ok(self.get(id, hw_counter)?.iter())
}
fn len(&self) -> usize {
@@ -73,16 +75,16 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
}
fn posting_list_len(&self, id: DimOffset, hw_counter: &HardwareCounterCell) -> Result<usize> {
Ok(self.get(id, hw_counter)?.len_to_end())
Ok(self.get(id, hw_counter)?.len())
}
fn files(path: &Path) -> Vec<std::path::PathBuf> {
InvertedIndexCompressedMmap::<W>::files(path)
InvertedIndexCompressedMmap::<W, Storage>::files(path)
}
fn immutable_files(path: &Path) -> Vec<std::path::PathBuf> {
// `InvertedIndexCompressedImmutableRam` is always immutable
InvertedIndexCompressedMmap::<W>::immutable_files(path)
InvertedIndexCompressedMmap::<W, Storage>::immutable_files(path)
}
fn remove(&mut self, _id: PointOffsetType, _old_vector: RemappedSparseVector) {
@@ -98,10 +100,7 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
panic!("Cannot upsert into a read-only RAM inverted index")
}
fn from_ram_index<P: AsRef<Path>>(
ram_index: Cow<InvertedIndexRam>,
_path: P,
) -> std::io::Result<Self> {
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, _path: P) -> Result<Self> {
let mut postings = Vec::with_capacity(ram_index.postings.len());
for old_posting_list in &ram_index.postings {
let mut new_posting_list = CompressedPostingBuilder::new();
@@ -141,6 +140,20 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
}
}
impl<W: Weight> InvertedIndexCompressedImmutableRam<W> {
#[inline]
fn get<'a>(
&'a self,
id: DimOffset,
hw_counter: &'a HardwareCounterCell,
) -> Result<CompressedPostingListView<'a, W>> {
let Some(posting) = self.postings.get(id as usize) else {
return Err(out_of_bounds(id, self.len()));
};
Ok(posting.view(hw_counter))
}
}
#[cfg(test)]
mod tests {
use tempfile::Builder;

View File

@@ -1,28 +1,30 @@
use std::borrow::Cow;
use std::fmt::Debug;
use std::io::{BufWriter, Write as _};
use std::marker::PhantomData;
use std::mem::size_of;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use common::counter::hardware_counter::HardwareCounterCell;
use common::ext::aligned_vec::ACow;
use common::fs::{atomic_save_json, read_json};
use common::mmap::{Advice, AdviceSetting, Madviseable, create_and_ensure_length, open_read_mmap};
use common::generic_consts::Random;
use common::mmap::{Advice, AdviceSetting, create_and_ensure_length};
#[expect(deprecated, reason = "legacy code")]
use common::mmap::{transmute_to_u8, transmute_to_u8_slice};
use common::storage_version::StorageVersion;
use common::types::PointOffsetType;
use common::universal_io::Result;
use memmap2::Mmap;
use common::universal_io::{OpenOptions, Populate, Result, UniversalRead, UniversalReadFs};
use serde::{Deserialize, Serialize};
use zerocopy::{FromBytes, Immutable, KnownLayout};
use super::inverted_index_compressed_immutable_ram::InvertedIndexCompressedImmutableRam;
use super::{INDEX_FILE_NAME, corrupted_index, out_of_bounds};
use crate::SearchScratchArena;
use crate::common::sparse_vector::RemappedSparseVector;
use crate::common::types::{DimId, DimOffset, Weight};
use crate::index::compressed_posting_list::{
CompressedPostingChunk, CompressedPostingListIterator, CompressedPostingListView,
CHUNK_SIZE, CompressedPostingChunk, CompressedPostingListIterator, CompressedPostingListView,
};
use crate::index::inverted_index::InvertedIndex;
use crate::index::inverted_index::inverted_index_ram::InvertedIndexRam;
@@ -53,14 +55,14 @@ pub struct InvertedIndexFileHeader {
/// Inverted flatten index from dimension id to posting list
#[derive(Debug)]
pub struct InvertedIndexCompressedMmap<W> {
pub struct InvertedIndexCompressedMmap<W, S: UniversalRead> {
path: PathBuf,
mmap: Arc<Mmap>,
storage: S,
pub file_header: InvertedIndexFileHeader,
_phantom: PhantomData<W>,
}
#[derive(Debug, Default, Clone, FromBytes, Immutable, KnownLayout)]
#[derive(Debug, Default, Copy, Clone, FromBytes, Immutable, KnownLayout)]
#[repr(C)]
struct PostingListFileHeader<W: Weight> {
pub ids_start: u64,
@@ -73,7 +75,24 @@ struct PostingListFileHeader<W: Weight> {
pub quantization_params: W::QuantizationParams,
}
impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
impl<W: Weight> PostingListFileHeader<W> {
fn postings_count(&self, remainders_end: u64) -> Option<usize> {
let chunks_bytes = self.chunks_count as usize * size_of::<CompressedPostingChunk<W>>();
let data_len = remainders_end.checked_sub(self.ids_start)? as usize;
let remainders_bytes = data_len.checked_sub(self.ids_len as usize + chunks_bytes)?;
let elem_size = size_of::<GenericPostingElement<W>>();
if !remainders_bytes.is_multiple_of(elem_size) {
return None;
}
let remainders_count = remainders_bytes / elem_size;
Some(self.chunks_count as usize * CHUNK_SIZE + remainders_count)
}
}
impl<W: Weight, S: UniversalRead + 'static> InvertedIndex for InvertedIndexCompressedMmap<W, S>
where
S::Fs: Default,
{
type Iter<'a> = CompressedPostingListIterator<'a, W>;
type Version = Version;
@@ -83,10 +102,10 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
}
fn open(path: &Path) -> Result<Self> {
Self::load(path)
Self::load(&S::Fs::default(), path)
}
fn save(&self, path: &Path) -> std::io::Result<()> {
fn save(&self, path: &Path) -> Result<()> {
debug_assert_eq!(path, self.path);
// If Self instance exists, it's either constructed by using `open()` (which reads index
@@ -102,9 +121,10 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
fn get<'a>(
&'a self,
id: DimOffset,
arena: &'a SearchScratchArena,
hw_counter: &'a HardwareCounterCell,
) -> Result<CompressedPostingListIterator<'a, W>> {
Ok(self.get(id, hw_counter)?.iter())
Ok(self.get(id, arena, hw_counter)?.iter())
}
fn len(&self) -> usize {
@@ -112,7 +132,10 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
}
fn posting_list_len(&self, id: DimOffset, hw_counter: &HardwareCounterCell) -> Result<usize> {
Ok(self.get(id, hw_counter)?.len())
let (header, remainders_end) = self.read_posting_header(id, hw_counter)?;
header
.postings_count(remainders_end)
.ok_or_else(corrupted_index)
}
fn files(path: &Path) -> Vec<PathBuf> {
@@ -140,12 +163,9 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
panic!("Cannot upsert into a read-only Mmap inverted index")
}
fn from_ram_index<P: AsRef<Path>>(
ram_index: Cow<InvertedIndexRam>,
path: P,
) -> std::io::Result<Self> {
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, path: P) -> Result<Self> {
let index = InvertedIndexCompressedImmutableRam::from_ram_index(ram_index, &path)?;
Self::convert_and_save(&index, path)
Self::convert_and_save(&S::Fs::default(), &index, path)
}
fn vector_count(&self) -> usize {
@@ -168,7 +188,7 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
}
}
impl<W: Weight> InvertedIndexCompressedMmap<W> {
impl<W: Weight, S: UniversalRead + Debug + 'static> InvertedIndexCompressedMmap<W, S> {
const HEADER_SIZE: usize = size_of::<PostingListFileHeader<W>>();
pub fn index_file_path(path: &Path) -> PathBuf {
@@ -182,42 +202,31 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
pub fn get<'a>(
&'a self,
id: DimId,
arena: &'a SearchScratchArena,
hw_counter: &'a HardwareCounterCell,
) -> Result<CompressedPostingListView<'a, W>> {
// check that the id is not out of bounds (posting_count includes the empty zeroth entry)
if id >= self.file_header.posting_count as DimId {
return Err(out_of_bounds(id, self.file_header.posting_count));
}
let (header, remainders_end) = self.read_posting_header(id, hw_counter)?;
let header = self.read_header(id)?.clone();
hw_counter.vector_io_read().incr_delta(Self::HEADER_SIZE);
let remainders_start = header.ids_start
+ u64::from(header.ids_len)
+ u64::from(header.chunks_count) * size_of::<CompressedPostingChunk<W>>() as u64;
let remainders_end = if id + 1 < self.file_header.posting_count as DimId {
self.read_header(id + 1)?.ids_start
} else {
self.mmap.len() as u64
let data_bytes = self.storage.read_bytes::<Random>(
header.ids_start..remainders_end,
align_of::<(CompressedPostingChunk<W>, GenericPostingElement<W>)>(),
)?;
let data = match data_bytes {
ACow::Borrowed(b) => b,
ACow::Owned(avec) => arena.alloc(avec),
};
if remainders_end
.checked_sub(remainders_start)
.is_some_and(|len| len % size_of::<GenericPostingElement<W>>() as u64 != 0)
{
return Err(corrupted_index());
}
let id_data = self.byte_slice(header.ids_start, u64::from(header.ids_len))?;
let chunks = <[CompressedPostingChunk<W>]>::ref_from_bytes(self.byte_slice(
header.ids_start + u64::from(header.ids_len),
u64::from(header.chunks_count) * size_of::<CompressedPostingChunk<W>>() as u64,
)?)
let ids_len = header.ids_len as usize;
let chunks_bytes = header.chunks_count as usize * size_of::<CompressedPostingChunk<W>>();
let id_data = data.get(..ids_len).ok_or_else(corrupted_index)?;
let chunks = <[CompressedPostingChunk<W>]>::ref_from_bytes(
data.get(ids_len..ids_len + chunks_bytes)
.ok_or_else(corrupted_index)?,
)
.map_err(|_| corrupted_index())?;
let remainders = <[GenericPostingElement<W>]>::ref_from_bytes(
&self.mmap[remainders_start as usize..remainders_end as usize],
data.get(ids_len + chunks_bytes..)
.ok_or_else(corrupted_index)?,
)
.map_err(|_| corrupted_index())?;
@@ -231,25 +240,42 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
))
}
fn read_header(&self, id: DimId) -> Result<&PostingListFileHeader<W>> {
let start = id as usize * Self::HEADER_SIZE;
let end = start + Self::HEADER_SIZE;
let bytes = self.mmap.get(start..end).ok_or_else(corrupted_index)?;
PostingListFileHeader::<W>::ref_from_bytes(bytes).map_err(|_| corrupted_index())
}
/// Read the header for `id`, plus the next header's `ids_start`
/// (which doubles as this entry's remainders end).
fn read_posting_header(
&self,
id: DimId,
hw_counter: &HardwareCounterCell,
) -> Result<(PostingListFileHeader<W>, u64)> {
if id >= self.file_header.posting_count as DimId {
return Err(out_of_bounds(id, self.file_header.posting_count));
}
fn byte_slice(&self, start: u64, len: u64) -> Result<&[u8]> {
let start = start as usize;
let end = start
.checked_add(len as usize)
.ok_or_else(corrupted_index)?;
self.mmap.get(start..end).ok_or_else(corrupted_index)
let header_start = u64::from(id) * Self::HEADER_SIZE as u64;
let has_next = id + 1 < self.file_header.posting_count as DimId;
let read_size = Self::HEADER_SIZE + if has_next { size_of::<u64>() } else { 0 };
let header_bytes = self.storage.read_bytes::<Random>(
header_start..header_start + read_size as u64,
align_of::<PostingListFileHeader<W>>(),
)?;
let (&header, rest) = PostingListFileHeader::<W>::ref_from_prefix(&header_bytes)
.map_err(|_| corrupted_index())?;
let remainders_end = if has_next {
*u64::ref_from_bytes(rest).map_err(|_| corrupted_index())?
} else {
self.storage.len::<u8>()?
};
hw_counter.vector_io_read().incr_delta(read_size);
Ok((header, remainders_end))
}
pub fn convert_and_save<P: AsRef<Path>>(
fs: &S::Fs,
index: &InvertedIndexCompressedImmutableRam<W>,
path: P,
) -> std::io::Result<Self> {
) -> Result<Self> {
let total_posting_headers_size =
index.postings.as_slice().len() * size_of::<PostingListFileHeader<W>>();
@@ -264,6 +290,7 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
.map(|p| p.view(&hw_counter).store_size().total)
.sum::<usize>();
let file_path = Self::index_file_path(path.as_ref());
// TODO(uio): use UIO for writing as well.
let file = create_and_ensure_length(file_path.as_ref(), file_length)?;
let mut buf = BufWriter::new(file);
@@ -312,34 +339,46 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
atomic_save_json(&Self::index_config_file_path(path.as_ref()), &file_header)?;
let storage = fs.open(
&file_path,
OpenOptions {
writeable: false,
need_sequential: false,
populate: Populate::No,
advice: AdviceSetting::Global,
},
Default::default(),
)?;
Ok(Self {
path: path.as_ref().to_owned(),
mmap: Arc::new(open_read_mmap(
file_path.as_ref(),
AdviceSetting::Global,
false,
)?),
storage,
file_header,
_phantom: PhantomData,
})
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
pub fn load<P: AsRef<Path>>(fs: &S::Fs, path: P) -> Result<Self> {
// read index config file
let config_file_path = Self::index_config_file_path(path.as_ref());
// if the file header does not exist, the index is malformed
let file_header: InvertedIndexFileHeader = read_json(&config_file_path)?;
// read index data into mmap
// open index data via universal IO
let file_path = Self::index_file_path(path.as_ref());
let mmap = open_read_mmap(
file_path.as_ref(),
AdviceSetting::from(Advice::Normal),
false,
let storage = fs.open(
&file_path,
OpenOptions {
writeable: false,
need_sequential: false,
populate: Populate::No,
advice: AdviceSetting::Advice(Advice::Normal),
},
Default::default(),
)?;
let mut index = Self {
path: path.as_ref().to_owned(),
mmap: Arc::new(mmap),
storage,
file_header,
_phantom: PhantomData,
};
@@ -348,63 +387,56 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
if index.file_header.total_sparse_size.is_none() {
index.file_header.total_sparse_size =
Some(index.calculate_total_sparse_size(&hw_counter));
Some(index.calculate_total_sparse_size(&hw_counter)?);
atomic_save_json(&config_file_path, &index.file_header)?;
}
Ok(index)
}
fn calculate_total_sparse_size(&self, hw_counter: &HardwareCounterCell) -> usize {
(0..self.file_header.posting_count as DimId)
.filter_map(|id| {
self.get(id, hw_counter)
.map(|posting| posting.store_size().total)
.ok()
})
.sum()
fn calculate_total_sparse_size(&self, hw_counter: &HardwareCounterCell) -> Result<usize> {
let mut total = 0;
let mut arena = SearchScratchArena::new_slow();
for id in 0..self.file_header.posting_count as DimId {
total += self.get(id, &arena, hw_counter)?.store_size().total;
arena.gc();
}
Ok(total)
}
/// Populate all pages in the mmap.
/// Block until all pages are populated.
pub fn populate(&self) -> std::io::Result<()> {
self.mmap.populate();
Ok(())
/// Populate the underlying storage in RAM cache. Block until completed.
pub fn populate(&self) -> Result<()> {
self.storage.populate()
}
/// Drop disk cache.
pub fn clear_cache(&self) -> std::io::Result<()> {
let Self {
path: _,
mmap,
file_header: _,
_phantom,
} = self;
mmap.clear_cache();
Ok(())
pub fn clear_cache(&self) -> Result<()> {
self.storage.clear_ram_cache()
}
}
#[cfg(test)]
mod tests {
use common::universal_io::MmapFile;
use tempfile::Builder;
use super::*;
use crate::common::types::QuantizedU8;
use crate::index::inverted_index::inverted_index_ram_builder::InvertedIndexBuilder;
fn compare_indexes<W: Weight>(
fn compare_indexes<S: UniversalRead + 'static, W: Weight>(
inverted_index_ram: &InvertedIndexCompressedImmutableRam<W>,
inverted_index_mmap: &InvertedIndexCompressedMmap<W>,
inverted_index_mmap: &InvertedIndexCompressedMmap<W, S>,
) {
let hw_counter = HardwareCounterCell::new();
let arena = SearchScratchArena::new_slow();
for id in 0..inverted_index_ram.postings.len() as DimId {
let posting_list_ram = inverted_index_ram
.postings
.get(id as usize)
.unwrap()
.view(&hw_counter);
let posting_list_mmap = inverted_index_mmap.get(id, &hw_counter).unwrap();
let posting_list_mmap = inverted_index_mmap.get(id, &arena, &hw_counter).unwrap();
let mmap_parts = posting_list_mmap.parts();
let ram_parts = posting_list_ram.parts();
@@ -415,13 +447,25 @@ mod tests {
#[test]
fn test_inverted_index_mmap() {
check_inverted_index_mmap::<f32>();
check_inverted_index_mmap::<half::f16>();
check_inverted_index_mmap::<u8>();
check_inverted_index_mmap::<QuantizedU8>();
check_inverted_index_mmap::<MmapFile, f32>();
check_inverted_index_mmap::<MmapFile, half::f16>();
check_inverted_index_mmap::<MmapFile, u8>();
check_inverted_index_mmap::<MmapFile, QuantizedU8>();
#[cfg(target_os = "linux")]
{
use common::universal_io::IoUringFile;
check_inverted_index_mmap::<IoUringFile, f32>();
check_inverted_index_mmap::<IoUringFile, half::f16>();
check_inverted_index_mmap::<IoUringFile, u8>();
check_inverted_index_mmap::<IoUringFile, QuantizedU8>();
}
}
fn check_inverted_index_mmap<W: Weight>() {
fn check_inverted_index_mmap<S: UniversalRead + 'static, W: Weight>()
where
S::Fs: Default,
{
let hw_counter = HardwareCounterCell::new();
// skip 4th dimension
@@ -446,7 +490,8 @@ mod tests {
let tmp_dir_path = Builder::new().prefix("test_index_dir2").tempdir().unwrap();
{
let inverted_index_mmap = InvertedIndexCompressedMmap::<W>::convert_and_save(
let inverted_index_mmap = InvertedIndexCompressedMmap::<W, S>::convert_and_save(
&S::Fs::default(),
&inverted_index_ram,
&tmp_dir_path,
)
@@ -454,22 +499,25 @@ mod tests {
compare_indexes(&inverted_index_ram, &inverted_index_mmap);
}
let inverted_index_mmap = InvertedIndexCompressedMmap::<W>::load(&tmp_dir_path).unwrap();
let index =
InvertedIndexCompressedMmap::<W, MmapFile>::load(&Default::default(), &tmp_dir_path)
.unwrap();
// posting_count: 0th entry is always empty + 1st + 2nd + 3rd + 4th empty + 5th
assert_eq!(inverted_index_mmap.file_header.posting_count, 6);
assert_eq!(inverted_index_mmap.file_header.vector_count, 9);
assert_eq!(index.file_header.posting_count, 6);
assert_eq!(index.file_header.vector_count, 9);
compare_indexes(&inverted_index_ram, &inverted_index_mmap);
compare_indexes(&inverted_index_ram, &index);
assert!(inverted_index_mmap.get(0, &hw_counter).unwrap().is_empty()); // the first entry is always empty as dimension ids start at 1
assert_eq!(inverted_index_mmap.get(1, &hw_counter).unwrap().len(), 9);
assert_eq!(inverted_index_mmap.get(2, &hw_counter).unwrap().len(), 4);
assert_eq!(inverted_index_mmap.get(3, &hw_counter).unwrap().len(), 3);
assert!(inverted_index_mmap.get(4, &hw_counter).unwrap().is_empty()); // return empty posting list info for intermediary empty ids
assert_eq!(inverted_index_mmap.get(5, &hw_counter).unwrap().len(), 2);
// index after the last values are None
assert!(inverted_index_mmap.get(6, &hw_counter).is_err());
assert!(inverted_index_mmap.get(7, &hw_counter).is_err());
assert!(inverted_index_mmap.get(100, &hw_counter).is_err());
let arena = SearchScratchArena::new_slow();
assert!(index.get(0, &arena, &hw_counter).unwrap().is_empty()); // the first entry is always empty as dimension ids start at 1
assert_eq!(index.get(1, &arena, &hw_counter).unwrap().len(), 9);
assert_eq!(index.get(2, &arena, &hw_counter).unwrap().len(), 4);
assert_eq!(index.get(3, &arena, &hw_counter).unwrap().len(), 3);
assert!(index.get(4, &arena, &hw_counter).unwrap().is_empty()); // return empty posting list info for intermediary empty ids
assert_eq!(index.get(5, &arena, &hw_counter).unwrap().len(), 2);
// index after the last values are errors (out of bounds)
assert!(index.get(6, &arena, &hw_counter).is_err());
assert!(index.get(7, &arena, &hw_counter).is_err());
assert!(index.get(100, &arena, &hw_counter).is_err());
}
}

View File

@@ -52,13 +52,14 @@ impl InvertedIndex for InvertedIndexRam {
panic!("InvertedIndexRam is not supposed to be loaded");
}
fn save(&self, _path: &Path) -> std::io::Result<()> {
fn save(&self, _path: &Path) -> Result<()> {
panic!("InvertedIndexRam is not supposed to be saved");
}
fn get<'a>(
&'a self,
id: DimOffset,
_arena: &'a crate::SearchScratchArena,
_hw_counter: &'a HardwareCounterCell,
) -> Result<PostingListIterator<'a>> {
Ok(self.get(id)?.iter())
@@ -104,10 +105,7 @@ impl InvertedIndex for InvertedIndexRam {
self.upsert(id, vector, old_vector);
}
fn from_ram_index<P: AsRef<Path>>(
ram_index: Cow<InvertedIndexRam>,
_path: P,
) -> std::io::Result<Self> {
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, _path: P) -> Result<Self> {
Ok(ram_index.into_owned())
}

View File

@@ -8,6 +8,7 @@ use common::types::PointOffsetType;
use common::universal_io::{Result, UniversalIoError};
use super::posting_list_common::PostingListIter;
use crate::SearchScratchArena;
use crate::common::sparse_vector::RemappedSparseVector;
use crate::common::types::DimOffset;
use crate::index::inverted_index::inverted_index_ram::InvertedIndexRam;
@@ -36,12 +37,13 @@ pub trait InvertedIndex: Sized + Debug + 'static {
fn open(path: &Path) -> Result<Self>;
/// Save index
fn save(&self, path: &Path) -> std::io::Result<()>;
fn save(&self, path: &Path) -> Result<()>;
/// Get posting list for dimension id
fn get<'a>(
&'a self,
id: DimOffset,
arena: &'a SearchScratchArena,
hw_counter: &'a HardwareCounterCell,
) -> Result<Self::Iter<'a>>;
@@ -72,10 +74,7 @@ pub trait InvertedIndex: Sized + Debug + 'static {
);
/// Create inverted index from ram index
fn from_ram_index<P: AsRef<Path>>(
ram_index: Cow<InvertedIndexRam>,
path: P,
) -> std::io::Result<Self>;
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, path: P) -> Result<Self>;
/// Number of indexed vectors
fn vector_count(&self) -> usize;

View File

@@ -4,11 +4,11 @@ use std::sync::atomic::Ordering::Relaxed;
use common::counter::hardware_counter::HardwareCounterCell;
use common::top_k::TopK;
use common::types::{PointOffsetType, ScoredPointOffset};
use common::types::{PointOffsetType, ScoreType, ScoredPointOffset};
use common::universal_io::Result;
use super::posting_list_common::PostingListIter;
use crate::common::scores_memory_pool::PooledScoresHandle;
use crate::SearchScratch;
use crate::common::sparse_vector::{RemappedSparseVector, score_vectors};
use crate::common::types::{DimId, DimWeight};
use crate::index::inverted_index::InvertedIndex;
@@ -24,7 +24,7 @@ pub struct IndexedPostingListIterator<T: PostingListIter> {
/// Making this larger makes the search faster but uses more (pooled) memory
const ADVANCE_BATCH_SIZE: usize = 10_000;
pub struct SearchContext<'a, 'b, T: PostingListIter = PostingListIterator<'a>> {
pub struct SearchContext<'a, T: PostingListIter = PostingListIterator<'a>> {
postings_iterators: Vec<IndexedPostingListIterator<T>>,
query: RemappedSparseVector,
top: usize,
@@ -32,27 +32,28 @@ pub struct SearchContext<'a, 'b, T: PostingListIter = PostingListIterator<'a>> {
top_results: TopK,
min_record_id: Option<PointOffsetType>, // min_record_id ids across all posting lists
max_record_id: PointOffsetType, // max_record_id ids across all posting lists
pooled: PooledScoresHandle<'b>, // handle to pooled scores
/// Scores buffer from [`SearchScratch`].
scores: &'a mut Vec<ScoreType>,
use_pruning: bool,
hardware_counter: &'a HardwareCounterCell,
}
impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
impl<'a, T: PostingListIter> SearchContext<'a, T> {
pub fn new(
query: RemappedSparseVector,
top: usize,
inverted_index: &'a impl InvertedIndex<Iter<'a> = T>,
pooled: PooledScoresHandle<'b>,
scratch: &'a mut SearchScratch<'_>,
is_stopped: &'a AtomicBool,
hardware_counter: &'a HardwareCounterCell,
) -> Result<SearchContext<'a, 'b, T>> {
) -> Result<SearchContext<'a, T>> {
let mut postings_iterators = Vec::new();
// track min and max record ids across all posting lists
let mut max_record_id = 0;
let mut min_record_id = u32::MAX;
// iterate over query indices
for (query_weight_offset, id) in query.indices.iter().enumerate() {
let mut it = inverted_index.get(*id, hardware_counter)?;
let mut it = inverted_index.get(*id, &scratch.arena, hardware_counter)?;
if let (Some(first), Some(last_id)) = (it.peek(), it.last_id()) {
// check if new min
let min_record_id_posting = first.record_id;
@@ -87,7 +88,7 @@ impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
top_results,
min_record_id,
max_record_id,
pooled,
scores: &mut scratch.scores,
use_pruning,
hardware_counter,
})
@@ -158,13 +159,13 @@ impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
) {
// init batch scores
let batch_len = batch_last_id - batch_start_id + 1;
self.pooled.scores.clear(); // keep underlying allocated memory
self.pooled.scores.resize(batch_len as usize, 0.0);
self.scores.clear(); // keep underlying allocated memory
self.scores.resize(batch_len as usize, 0.0);
for posting in self.postings_iterators.iter_mut() {
posting.posting_list_iterator.for_each_till_id(
batch_last_id,
self.pooled.scores.as_mut_slice(),
self.scores.as_mut_slice(),
#[inline(always)]
|scores, id, weight| {
let element_score = weight * posting.query_weight;
@@ -176,7 +177,7 @@ impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
);
}
for (local_index, &score) in self.pooled.scores.iter().enumerate() {
for (local_index, &score) in self.scores.iter().enumerate() {
// publish only the non-zero scores above the current min to beat
if score != 0.0 && score > self.top_results.threshold() {
let real_id = batch_start_id + local_index as PointOffsetType;

View File

@@ -1,11 +1,10 @@
use std::borrow::Cow;
use std::sync::OnceLock;
use common::types::PointOffsetType;
use common::universal_io::MmapFile;
use rand::{RngExt, SeedableRng};
use tempfile::TempDir;
use crate::common::scores_memory_pool::{PooledScoresHandle, ScoresMemoryPool};
use crate::common::sparse_vector::RemappedSparseVector;
use crate::common::types::{DimOffset, Weight};
use crate::index::inverted_index::InvertedIndex;
@@ -13,14 +12,6 @@ use crate::index::inverted_index::inverted_index_compressed_mmap::InvertedIndexC
use crate::index::inverted_index::inverted_index_ram::InvertedIndexRam;
use crate::index::inverted_index::inverted_index_ram_builder::InvertedIndexBuilder;
static TEST_SCORES_POOL: OnceLock<ScoresMemoryPool> = OnceLock::new();
pub fn get_pooled_scores() -> PooledScoresHandle<'static> {
TEST_SCORES_POOL
.get_or_init(ScoresMemoryPool::default)
.get()
}
pub struct TestIndex<I: InvertedIndex> {
pub index: I,
_temp_dir: TempDir,
@@ -78,7 +69,7 @@ pub fn generate_sparse_index<W, R>(
density: usize,
vocab1: usize,
vocab2: usize,
) -> TestIndex<InvertedIndexCompressedMmap<W>>
) -> TestIndex<InvertedIndexCompressedMmap<W, MmapFile>>
where
W: Weight + 'static,
R: rand::Rng,
@@ -98,7 +89,7 @@ pub fn build_index<W>(
density: usize,
vocab1: usize,
vocab2: usize,
) -> TestIndex<InvertedIndexCompressedMmap<W>>
) -> TestIndex<InvertedIndexCompressedMmap<W, MmapFile>>
where
W: Weight + 'static,
{

View File

@@ -4,21 +4,23 @@ use common::counter::hardware_accumulator::HwMeasurementAcc;
use common::types::PointOffsetType;
use itertools::Itertools;
use crate::SearchScratch;
use crate::common::sparse_vector::RemappedSparseVector;
use crate::index::inverted_index::InvertedIndex;
use crate::index::search_context::SearchContext;
use crate::index::tests::common::{build_index, get_pooled_scores, match_all};
use crate::index::tests::common::{build_index, match_all};
fn do_search<I: InvertedIndex>(index: &I, query: RemappedSparseVector) -> HwMeasurementAcc {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let top = 10;
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
query,
top,
index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -41,11 +43,12 @@ fn do_plain_search<I: InvertedIndex>(
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let top = 10;
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
query,
top,
index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)

View File

@@ -2,21 +2,23 @@ use std::sync::atomic::AtomicBool;
use common::counter::hardware_accumulator::HwMeasurementAcc;
use crate::SearchScratch;
use crate::common::sparse_vector::RemappedSparseVector;
use crate::index::inverted_index::InvertedIndex;
use crate::index::search_context::SearchContext;
use crate::index::tests::common::{build_index, get_pooled_scores, match_all};
use crate::index::tests::common::{build_index, match_all};
fn query<I: InvertedIndex>(index: &I, query: RemappedSparseVector) {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let top = 10;
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
query.clone(),
top,
index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -24,12 +26,13 @@ fn query<I: InvertedIndex>(index: &I, query: RemappedSparseVector) {
let result = search_context.search(&match_all);
let docs: Vec<_> = result.iter().map(|x| x.idx).collect();
drop(search_context);
let mut search_context = SearchContext::new(
query,
top,
index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)

View File

@@ -3,15 +3,17 @@
mod tests {
use std::any::TypeId;
use std::borrow::Cow;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use common::counter::hardware_accumulator::HwMeasurementAcc;
use common::counter::hardware_counter::HardwareCounterCell;
use common::types::{PointOffsetType, ScoredPointOffset};
#[cfg(target_os = "linux")]
use common::universal_io::IoUringFile;
use common::universal_io::MmapFile;
use tempfile::TempDir;
use crate::common::scores_memory_pool::{PooledScoresHandle, ScoresMemoryPool};
use crate::SearchScratch;
use crate::common::sparse_vector::RemappedSparseVector;
use crate::common::types::QuantizedU8;
use crate::index::inverted_index::InvertedIndex;
@@ -38,28 +40,36 @@ mod tests {
#[instantiate_tests(<InvertedIndexCompressedImmutableRam<QuantizedU8>>)]
mod iram_q8 {}
#[instantiate_tests(<InvertedIndexCompressedMmap<f32>>)]
#[instantiate_tests(<InvertedIndexCompressedMmap<f32, MmapFile>>)]
mod mmap_f32 {}
#[instantiate_tests(<InvertedIndexCompressedMmap<half::f16>>)]
#[instantiate_tests(<InvertedIndexCompressedMmap<half::f16, MmapFile>>)]
mod mmap_f16 {}
#[instantiate_tests(<InvertedIndexCompressedMmap<u8>>)]
#[instantiate_tests(<InvertedIndexCompressedMmap<u8, MmapFile>>)]
mod mmap_u8 {}
#[instantiate_tests(<InvertedIndexCompressedMmap<QuantizedU8>>)]
#[instantiate_tests(<InvertedIndexCompressedMmap<QuantizedU8, MmapFile>>)]
mod mmap_q8 {}
#[cfg(target_os = "linux")]
#[instantiate_tests(<InvertedIndexCompressedMmap<f32, IoUringFile>>)]
mod uring_f32 {}
#[cfg(target_os = "linux")]
#[instantiate_tests(<InvertedIndexCompressedMmap<half::f16, IoUringFile>>)]
mod uring_f16 {}
#[cfg(target_os = "linux")]
#[instantiate_tests(<InvertedIndexCompressedMmap<u8, IoUringFile>>)]
mod uring_u8 {}
#[cfg(target_os = "linux")]
#[instantiate_tests(<InvertedIndexCompressedMmap<QuantizedU8, IoUringFile>>)]
mod uring_q8 {}
// --- End of test instantiations ---
static TEST_SCORES_POOL: OnceLock<ScoresMemoryPool> = OnceLock::new();
fn get_pooled_scores() -> PooledScoresHandle<'static> {
TEST_SCORES_POOL
.get_or_init(ScoresMemoryPool::default)
.get()
}
/// Match all filter condition for testing
fn match_all(_p: PointOffsetType) -> bool {
true
@@ -88,7 +98,9 @@ mod tests {
fn round_scores<I: 'static>(mut scores: Vec<ScoredPointOffset>) -> Vec<ScoredPointOffset> {
let errors_allowed_for = [
TypeId::of::<InvertedIndexCompressedImmutableRam<QuantizedU8>>(),
TypeId::of::<InvertedIndexCompressedMmap<QuantizedU8>>(),
TypeId::of::<InvertedIndexCompressedMmap<QuantizedU8, MmapFile>>(),
#[cfg(target_os = "linux")]
TypeId::of::<InvertedIndexCompressedMmap<QuantizedU8, IoUringFile>>(),
];
if errors_allowed_for.contains(&TypeId::of::<I>()) {
let precision = 0.25;
@@ -108,11 +120,12 @@ mod tests {
let hw_counter = HardwareCounterCell::disposable();
let is_stopped = AtomicBool::new(false);
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector::default(), // empty query vector
10,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hw_counter,
)
@@ -133,6 +146,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -140,7 +154,7 @@ mod tests {
},
10,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -192,6 +206,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -199,7 +214,7 @@ mod tests {
},
10,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -235,6 +250,7 @@ mod tests {
None,
);
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -242,7 +258,7 @@ mod tests {
},
10,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -290,6 +306,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -297,7 +314,7 @@ mod tests {
},
3,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -332,6 +349,7 @@ mod tests {
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -339,7 +357,7 @@ mod tests {
},
4,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -385,6 +403,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1],
@@ -392,7 +411,7 @@ mod tests {
},
1,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -420,6 +439,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -427,7 +447,7 @@ mod tests {
},
1,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -460,6 +480,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -467,7 +488,7 @@ mod tests {
},
1,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -498,6 +519,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -505,7 +527,7 @@ mod tests {
},
3,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -531,6 +553,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 2, 3],
@@ -538,7 +561,7 @@ mod tests {
},
3,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)
@@ -586,6 +609,7 @@ mod tests {
let is_stopped = AtomicBool::new(false);
let accumulator = HwMeasurementAcc::new();
let hardware_counter = accumulator.get_counter_cell();
let mut scratch = SearchScratch::new_for_test();
let mut search_context = SearchContext::new(
RemappedSparseVector {
indices: vec![1, 3],
@@ -593,7 +617,7 @@ mod tests {
},
3,
&index.index,
get_pooled_scores(),
&mut scratch,
&is_stopped,
&hardware_counter,
)

View File

@@ -1,2 +1,5 @@
pub mod common;
pub mod index;
pub(crate) mod search_scratch;
pub use search_scratch::{SearchScratch, SearchScratchArena, SearchScratchPool};

View File

@@ -0,0 +1,102 @@
use common::defaults::POOL_KEEP_LIMIT;
use common::ext::aligned_vec::{AVec, RuntimeAlign};
use common::types::ScoreType;
use parking_lot::Mutex;
use typed_arena::Arena;
#[derive(Debug)]
pub struct SearchScratchPool {
pool: Mutex<Vec<SearchScratchScores>>,
}
impl SearchScratchPool {
#[expect(clippy::new_without_default)]
pub fn new() -> SearchScratchPool {
SearchScratchPool {
pool: Mutex::new(Vec::with_capacity(*POOL_KEEP_LIMIT)),
}
}
/// Take a single [`SearchScratch`] from the pool.
pub fn get(&self) -> SearchScratch<'_> {
let scores = self.pool.lock().pop().unwrap_or_default();
SearchScratch {
pool: self,
scores,
arena: SearchScratchArena::new_slow(),
}
}
}
/// Scratch space for use by [`crate::index::search_context::SearchContext`].
pub struct SearchScratch<'a> {
/// The pool to return this scratch on drop.
pool: &'a SearchScratchPool,
/// Used for batched scoring.
pub(crate) scores: SearchScratchScores,
/// Used to own/store posting list bytes while reading them from the file.
pub(crate) arena: SearchScratchArena,
}
impl SearchScratch<'_> {
#[cfg(test)]
pub fn new_for_test() -> SearchScratch<'static> {
use std::sync::OnceLock;
static POOL: OnceLock<SearchScratchPool> = OnceLock::new();
POOL.get_or_init(SearchScratchPool::new).get()
}
}
type SearchScratchScores = Vec<ScoreType>;
impl Drop for SearchScratch<'_> {
fn drop(&mut self) {
let SearchScratch {
pool: SearchScratchPool { pool },
scores,
arena: _,
} = self;
let mut pool = pool.lock();
if pool.len() < *POOL_KEEP_LIMIT {
pool.push(std::mem::take(scores));
}
}
}
/// Hacky wrapper around [`Arena`] with [`Self::gc`] method.
///
/// TODO: get rid of this struct once [`Arena`] gets `clear()` method, and use
/// type alias instead.
/// https://github.com/thomcc/rust-typed-arena/issues/64
// pub type SearchScratchArena = Arena<AVec<u8, RuntimeAlign>>;
pub struct SearchScratchArena(Arena<AVec<u8, RuntimeAlign>>);
impl SearchScratchArena {
/// Same as [`Arena::new`].
/// Renamed to `new_slow` to point out that [`Arena::new`] allocates.
pub fn new_slow() -> SearchScratchArena {
SearchScratchArena(Arena::new())
}
/// Free memory if the arena got too big.
/// Poor's man version of `Arena::clear()`.
pub fn gc(&mut self) {
// Too small => frequent reallocations
// Too big => memory bloat
const ARBITRARY_LIMIT: usize = 256;
if self.0.len() > ARBITRARY_LIMIT {
self.0 = Arena::new();
}
for vec in self.0.iter_mut() {
*vec = AVec::new(1);
}
}
}
impl std::ops::Deref for SearchScratchArena {
type Target = Arena<AVec<u8, RuntimeAlign>>;
fn deref(&self) -> &Arena<AVec<u8, RuntimeAlign>> {
&self.0
}
}