Use BitvecFlags in vector storages (#6994)

* put all flags impls in a single module

* use in appendable_dense_vector_storage

* use in appendable_mmap_multi_dense_storage

* use in mmap_sparse_vector_storage

* clippy

* Add comment we cannot rely on length of deleted vectors bitvec

---------

Co-authored-by: timvisee <tim@visee.me>
This commit is contained in:
Luis Cossío
2025-08-11 13:17:39 +02:00
committed by timvisee
co-authored by timvisee
parent 75b09284eb
commit f2d45b558d
13 changed files with 100 additions and 86 deletions
@@ -4,10 +4,10 @@ use bitvec::slice::BitSlice;
use bitvec::vec::BitVec;
use common::types::PointOffsetType;
use super::buffered_dynamic_flags::BufferedDynamicFlags;
use super::dynamic_mmap_flags::DynamicMmapFlags;
use crate::common::Flusher;
use crate::common::buffered_dynamic_flags::BufferedDynamicFlags;
use crate::common::operation_error::OperationResult;
use crate::vector_storage::dense::dynamic_mmap_flags::DynamicMmapFlags;
/// A buffered, growable, and persistent bitslice with a separate in-memory bitvec.
///
@@ -16,6 +16,7 @@ use crate::vector_storage::dense::dynamic_mmap_flags::DynamicMmapFlags;
/// Changes are buffered until explicitly flushed.
///
/// [1]: super::roaring_flags::RoaringFlags
#[derive(Debug)]
pub struct BitvecFlags {
/// Buffered persisted flags.
storage: BufferedDynamicFlags,
@@ -71,17 +72,19 @@ impl BitvecFlags {
.map(|index| index as PointOffsetType)
}
#[inline]
pub fn count_trues(&self) -> usize {
self.bitvec.count_ones()
}
#[inline]
pub fn count_falses(&self) -> usize {
self.bitvec.count_zeros()
}
/// Set the value of a flag at the given index.
/// Set the value of a flag at the given index, grows the bitvec if needed.
/// Returns the previous value of the flag.
pub fn set(&mut self, index: PointOffsetType, value: bool) {
pub fn set(&mut self, index: PointOffsetType, value: bool) -> bool {
// queue write in buffer
self.storage.buffer_set(index, value);
@@ -93,7 +96,7 @@ impl BitvecFlags {
}
// update bitmap
self.bitvec.set(index_usize, value)
self.bitvec.replace(index_usize, value)
}
pub fn clear_cache(&self) -> OperationResult<()> {
@@ -114,8 +117,8 @@ impl BitvecFlags {
mod tests {
use common::types::PointOffsetType;
use crate::common::bitvec_flags::BitvecFlags;
use crate::vector_storage::dense::dynamic_mmap_flags::DynamicMmapFlags;
use crate::common::flags::bitvec_flags::BitvecFlags;
use crate::common::flags::dynamic_mmap_flags::DynamicMmapFlags;
#[test]
fn test_roaring_flags_consistency_after_persistence() {
@@ -5,13 +5,14 @@ use ahash::AHashMap;
use common::types::PointOffsetType;
use parking_lot::{Mutex, RwLock};
use super::dynamic_mmap_flags::DynamicMmapFlags;
use crate::common::Flusher;
use crate::common::operation_error::OperationResult;
use crate::vector_storage::dense::dynamic_mmap_flags::DynamicMmapFlags;
/// A buffered wrapper around DynamicMmapFlags that provides manual flushing, without interface for reading.
///
/// Changes are buffered until explicitly flushed.
#[derive(Debug)]
pub(crate) struct BufferedDynamicFlags {
/// Persisted flags.
storage: Arc<Mutex<DynamicMmapFlags>>,
@@ -83,8 +84,8 @@ mod tests {
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use crate::common::buffered_dynamic_flags::BufferedDynamicFlags;
use crate::vector_storage::dense::dynamic_mmap_flags::DynamicMmapFlags;
use crate::common::flags::buffered_dynamic_flags::BufferedDynamicFlags;
use crate::common::flags::dynamic_mmap_flags::DynamicMmapFlags;
#[test]
fn test_buffered_flags_growth_persistence() {
@@ -0,0 +1,384 @@
use std::cmp::max;
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::{fmt, fs};
use bitvec::prelude::BitSlice;
use common::counter::referenced_counter::HwMetricRefCounter;
use common::types::PointOffsetType;
use memmap2::MmapMut;
use memory::fadvise::clear_disk_cache;
use memory::madvise::{self, AdviceSetting, Madviseable as _};
use memory::mmap_ops::{create_and_ensure_length, open_write_mmap};
use memory::mmap_type::{MmapBitSlice, MmapType};
use crate::common::Flusher;
use crate::common::operation_error::{OperationError, OperationResult};
#[cfg(debug_assertions)]
const MINIMAL_MMAP_SIZE: usize = 128; // 128 bytes -> 1024 flags
#[cfg(not(debug_assertions))]
const MINIMAL_MMAP_SIZE: usize = 1024 * 1024; // 1Mb
const FLAGS_FILE: &str = "flags_a.dat";
const FLAGS_FILE_LEGACY: &str = "flags_b.dat";
const STATUS_FILE_NAME: &str = "status.dat";
fn status_file(directory: &Path) -> PathBuf {
directory.join(STATUS_FILE_NAME)
}
#[repr(C)]
struct DynamicMmapStatus {
/// Amount of flags (bits)
len: usize,
/// Should be 0 in the current version. Old versions used it to indicate which flags file
/// (flags_a.dat or flags_b.dat) is currently in use.
current_file_id: usize,
}
fn ensure_status_file(directory: &Path) -> OperationResult<MmapMut> {
let status_file = status_file(directory);
if !status_file.exists() {
let length = std::mem::size_of::<DynamicMmapStatus>();
create_and_ensure_length(&status_file, length)?;
}
Ok(open_write_mmap(&status_file, AdviceSetting::Global, false)?)
}
pub struct DynamicMmapFlags {
/// Current mmap'ed BitSlice for flags
flags: MmapBitSlice,
status: MmapType<DynamicMmapStatus>,
directory: PathBuf,
}
impl fmt::Debug for DynamicMmapFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DynamicMmapFlags")
.field("flags", &self.flags)
.field("status", &self.status)
.field("directory", &self.directory)
.finish_non_exhaustive()
}
}
/// Based on the number of flags determines the size of the mmap file.
fn mmap_capacity_bytes(num_flags: usize) -> usize {
let number_of_bytes = num_flags.div_ceil(u8::BITS as usize);
max(MINIMAL_MMAP_SIZE, number_of_bytes.next_power_of_two())
}
/// Based on the current length determines how many flags can fit into the mmap file without resizing it.
fn mmap_max_current_size(len: usize) -> usize {
let mmap_capacity_bytes = mmap_capacity_bytes(len);
mmap_capacity_bytes * u8::BITS as usize
}
impl DynamicMmapFlags {
pub fn len(&self) -> usize {
self.status.len
}
pub fn is_empty(&self) -> bool {
self.status.len == 0
}
pub fn open(directory: &Path, populate: bool) -> OperationResult<Self> {
fs::create_dir_all(directory)?;
let status_mmap = ensure_status_file(directory)?;
let mut status: MmapType<DynamicMmapStatus> = unsafe { MmapType::try_from(status_mmap)? };
if status.current_file_id != 0 {
// Migrate
fs::copy(
directory.join(FLAGS_FILE_LEGACY),
directory.join(FLAGS_FILE),
)?;
status.current_file_id = 0;
status.flusher()()?;
}
// Open first mmap
let flags = Self::open_mmap(status.len, directory, populate)?;
Ok(Self {
flags,
status,
directory: directory.to_owned(),
})
}
fn open_mmap(
num_flags: usize,
directory: &Path,
populate: bool,
) -> OperationResult<MmapBitSlice> {
let capacity_bytes = mmap_capacity_bytes(num_flags);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(directory.join(FLAGS_FILE))?;
file.set_len(capacity_bytes as u64)?;
let flags_mmap = unsafe { MmapMut::map_mut(&file)? };
drop(file);
flags_mmap.madvise(madvise::get_global())?;
if populate {
flags_mmap.populate();
} else {
#[cfg(unix)]
if let Err(err) = flags_mmap.advise(memmap2::Advice::WillNeed) {
log::error!("Failed to advise MADV_WILLNEED for deleted flags: {err}");
}
}
let flags = MmapBitSlice::try_from(flags_mmap, 0)?;
Ok(flags)
}
/// Set the length of the vector to the given value.
/// If the vector is grown, the new elements will be set to `false`.
///
/// NOTE: capacity can be up to 2x the current length.
///
/// Errors if the vector is shrunk.
pub fn set_len(&mut self, new_len: usize) -> OperationResult<()> {
debug_assert!(new_len >= self.status.len);
if new_len == self.status.len {
return Ok(());
}
if new_len < self.status.len {
return Err(OperationError::service_error(format!(
"Cannot shrink the mmap flags from {} to {new_len}",
self.status.len,
)));
}
// Capacity can be up to 2x the current length
let current_capacity = mmap_max_current_size(self.status.len);
if new_len > current_capacity {
// Flush the current mmaps before resizing
self.flags.flusher()()?;
// Don't read the whole file on resize
let populate = false;
let flags = Self::open_mmap(new_len, &self.directory, populate)?;
// Swap operation. It is important this section is not interrupted by errors.
self.flags = flags;
}
self.status.len = new_len;
Ok(())
}
pub fn get<TKey>(&self, key: TKey) -> bool
where
TKey: num_traits::cast::AsPrimitive<usize>,
{
let key: usize = key.as_();
if key >= self.status.len {
return false;
}
self.flags[key]
}
/// Count number of set flags
pub fn count_flags(&self) -> usize {
// Take a bitslice of our set length, count ones in it
// This uses bit-indexing, returning a new bitslice, extra bits within capacity are not counted
self.flags[..self.status.len].count_ones()
}
/// Set the `true` value of the flag at the given index.
/// Ignore the call if the index is out of bounds.
///
/// Returns previous value of the flag.
pub fn set<TKey>(&mut self, key: TKey, value: bool) -> bool
where
TKey: num_traits::cast::AsPrimitive<usize>,
{
let key: usize = key.as_();
debug_assert!(key < self.status.len);
if key >= self.status.len {
return false;
}
self.flags.replace(key, value)
}
/// This method will set the flag at the given index to the given value.
/// If current length is not enough, it will resize the flags with amortized cost (x2)
/// All new flags will be set to false.
///
/// Returns previous value of the flag.
pub fn set_with_resize<TKey>(
&mut self,
key: TKey,
value: bool,
hw_counter_ref: HwMetricRefCounter,
) -> OperationResult<bool>
where
TKey: num_traits::cast::AsPrimitive<usize>,
{
// Measure write of single bool.
hw_counter_ref.incr_delta(size_of::<bool>());
let key: usize = key.as_();
if key >= self.status.len {
if value {
let new_len = key + 1;
hw_counter_ref.incr_delta(new_len - self.status.len);
self.set_len(new_len)?;
} else {
// Default value is false, so we don't need to resize
return Ok(false);
}
}
Ok(self.flags.replace(key, value))
}
pub fn flusher(&self) -> Flusher {
Box::new({
let flags_flusher = self.flags.flusher();
let status_flusher = self.status.flusher();
move || {
flags_flusher()?;
status_flusher()?;
Ok(())
}
})
}
pub fn get_bitslice(&self) -> &BitSlice {
// Take subslice with actual length, bitslice may be larger due to extra allocated capacity
// See `mmap_capacity_bytes`
&self.flags[..self.len()]
}
// no immutable files, everything is mutable
pub fn files(&self) -> Vec<PathBuf> {
vec![
status_file(&self.directory),
self.directory.join(FLAGS_FILE),
]
}
/// Iterate over all "true" flags
pub fn iter_trues(&self) -> impl Iterator<Item = PointOffsetType> + '_ {
self.flags.iter_ones().map(|x| x as PointOffsetType)
}
/// Populate all pages in the mmap.
/// Block until all pages are populated.
pub fn populate(&self) -> OperationResult<()> {
self.flags.populate()?;
Ok(())
}
/// Drop disk cache.
pub fn clear_cache(&self) -> OperationResult<()> {
let flags_file = self.directory.join(FLAGS_FILE);
clear_disk_cache(&flags_file)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::iter;
use rand::prelude::StdRng;
use rand::{Rng, SeedableRng};
use tempfile::Builder;
use super::*;
#[test]
fn test_bitflags_saving() {
let dir = Builder::new().prefix("storage_dir").tempdir().unwrap();
let num_flags = 5000;
let mut rng = StdRng::seed_from_u64(42);
let random_flags: Vec<bool> = iter::repeat_with(|| rng.random()).take(num_flags).collect();
{
let mut dynamic_flags = DynamicMmapFlags::open(dir.path(), false).unwrap();
dynamic_flags.set_len(num_flags).unwrap();
random_flags
.iter()
.enumerate()
.filter(|(_, flag)| **flag)
.for_each(|(i, _)| assert!(!dynamic_flags.set(i, true)));
dynamic_flags.set_len(num_flags * 2).unwrap();
random_flags
.iter()
.enumerate()
.filter(|(_, flag)| !*flag)
.for_each(|(i, _)| assert!(!dynamic_flags.set(num_flags + i, true)));
dynamic_flags.flusher()().unwrap();
}
{
let dynamic_flags = DynamicMmapFlags::open(dir.path(), true).unwrap();
assert_eq!(dynamic_flags.status.len, num_flags * 2);
for (i, flag) in random_flags.iter().enumerate() {
assert_eq!(dynamic_flags.get(i), *flag);
assert_eq!(dynamic_flags.get(num_flags + i), !*flag);
}
}
}
#[test]
fn test_bitflags_counting() {
let dir = Builder::new().prefix("storage_dir").tempdir().unwrap();
let num_flags = 5003; // Prime number, not byte aligned
let mut rng = StdRng::seed_from_u64(42);
// Create randomized dynamic mmap flags to test counting
let mut dynamic_flags = DynamicMmapFlags::open(dir.path(), true).unwrap();
dynamic_flags.set_len(num_flags).unwrap();
let random_flags: Vec<bool> = iter::repeat_with(|| rng.random()).take(num_flags).collect();
random_flags
.iter()
.enumerate()
.filter(|(_, flag)| **flag)
.for_each(|(i, _)| assert!(!dynamic_flags.set(i, true)));
dynamic_flags.flusher()().unwrap();
// Test count flags method
let count = dynamic_flags.count_flags();
// Compare against manually counting every flag
let mut manual_count = 0;
for i in 0..num_flags {
if dynamic_flags.get(i) {
manual_count += 1;
}
}
assert_eq!(count, manual_count);
}
#[test]
fn test_capacity() {
assert_eq!(mmap_capacity_bytes(0), 128);
assert_eq!(mmap_capacity_bytes(1), 128);
assert_eq!(mmap_capacity_bytes(1023), 128);
assert_eq!(mmap_capacity_bytes(1024), 128);
assert_eq!(mmap_capacity_bytes(1025), 256);
assert_eq!(mmap_capacity_bytes(10000), 2048);
}
}
+12
View File
@@ -0,0 +1,12 @@
//! Different flavors of flags structures, akin to a Vec<bool>, but persistent and efficient.
//!
//! Here's a brief overview of the different flavors of flags structures:
//! - `dynamic_mmap_flags`: Base implementation of storage in mmapped files.
//! - `buffered_dynamic_flags`: Builds on top of `dynamic_mmap_flags` to provide buffered writes.
//! - `bitvec_flags`: `buffered_dynamic_flags` with in-memory bitvec for reads.
//! - `roaring_flags`: `buffered_dynamic_flags` with in-memory roaring bitmap for reads.
pub mod bitvec_flags;
mod buffered_dynamic_flags;
pub mod dynamic_mmap_flags;
pub mod roaring_flags;
@@ -3,10 +3,10 @@ use std::path::PathBuf;
use common::types::PointOffsetType;
use roaring::RoaringBitmap;
use super::buffered_dynamic_flags::BufferedDynamicFlags;
use super::dynamic_mmap_flags::DynamicMmapFlags;
use crate::common::Flusher;
use crate::common::buffered_dynamic_flags::BufferedDynamicFlags;
use crate::common::operation_error::OperationResult;
use crate::vector_storage::dense::dynamic_mmap_flags::DynamicMmapFlags;
/// A buffered, growable, and persistent bitslice with fast in-memory roaring bitmap.
///
@@ -117,8 +117,8 @@ impl RoaringFlags {
mod tests {
use common::types::PointOffsetType;
use crate::common::roaring_flags::RoaringFlags;
use crate::vector_storage::dense::dynamic_mmap_flags::DynamicMmapFlags;
use crate::common::flags::dynamic_mmap_flags::DynamicMmapFlags;
use crate::common::flags::roaring_flags::RoaringFlags;
#[test]
fn test_roaring_flags_consistency_after_persistence() {
+1 -3
View File
@@ -1,14 +1,12 @@
pub mod anonymize;
pub mod bitvec_flags;
pub mod buffered_dynamic_flags;
pub mod error_logging;
pub mod flags;
pub mod macros;
pub mod mmap_bitslice_buffered_update_wrapper;
pub mod mmap_slice_buffered_update_wrapper;
pub mod operation_error;
pub mod operation_time_statistics;
pub mod reciprocal_rank_fusion;
pub mod roaring_flags;
#[cfg(feature = "rocksdb")]
pub mod rocksdb_buffered_delete_wrapper;
#[cfg(feature = "rocksdb")]