mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-03 00:20:57 -05:00
* feat: `io_uring` setting to control which components use the io_uring backend A few components have both an mmap and an io_uring variant reading the very same files: the immutable dense vector storages, the single-file TurboQuant storage, and the mmap payload storage. Until now the choice was a side effect of `async_scorer` — a vector-search knob — plus, for the payload storage, a feature flag that was parked off because io_uring is ~2x slower than mmap when the data fits the page cache (#9310, #9409). Add `storage.performance.io_uring`, optional, with two modes: - unset (default): unchanged behaviour. The vector storages keep following `async_scorer`; the payload storage stays on mmap. - `disabled`: no component uses io_uring. - `auto`: a component uses io_uring when its memory placement is `cold` (data is left on disk, so reads hit the disk and there is something to gain), its feature flag allows it, and the kernel supports io_uring. Components meant to sit in RAM keep using mmap. The decision lives in one place, `segment::common::io_uring::use_io_uring`, so the openers no longer each reach for the async-scorer global. Kernel support is now probed up front through `is_io_uring_supported()` instead of opening a file and falling back on error. `async_payload_storage` now defaults to on: it no longer decides anything by itself, it only lifts the ban, and the payload storage no longer follows `async_scorer` at all — so turning it on cannot silently move an existing `async_scorer: true` deployment onto the slower path. Which backend a component ended up on depends on the config, the placement and the kernel at once, so report it in `SegmentInfo`: `vector_data[name].io_backend` and `payload_storage_io_backend`, both `"mmap" | "io_uring"`, absent for components that have no such choice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Trim comments, drop trivial tests Two tests were only restating their own implementation: `test_mode_round_trip` round-tripped the encode/decode pair next to it, and `test_io_uring_config` checked that serde deserializes a two-variant enum. The mode matrix test stays, it is the one that pins the semantics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Flatten `IoBackend` in OpenAPI, derive `JsonSchema` for `IoUringMode` Per-variant doc comments on a plain string enum make schemars emit a `oneOf` of anonymous single-value objects instead of a flat `enum`. Move the variant descriptions into the enum doc, as `Memory` and friends already do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Update lib/segment/src/vector_storage/turbo/turbo_vector_storage.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Update lib/segment/src/types.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * upd openapi schema * Update lib/common/common/src/flags.rs Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com> * Require kernel io_uring support in the async-scorer fallback `use_io_uring` returned `get_async_scorer()` verbatim when the `io_uring` setting is unset, so an enabled async scorer on a kernel without io_uring opened the io_uring storage, failed, and fell back to mmap with an error log per segment. Gate that branch on `is_io_uring_supported()` too, like `Auto` already is, so the component just stays on mmap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * upd openapi schema --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com>
130 lines
4.1 KiB
Rust
130 lines
4.1 KiB
Rust
use std::array;
|
|
use std::sync::Arc;
|
|
|
|
use atomic_refcell::AtomicRefCell;
|
|
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
|
use rand::RngExt;
|
|
use rand::distr::StandardUniform;
|
|
use rand::rngs::SmallRng;
|
|
use segment::data_types::vectors::{DenseVector, QueryVector};
|
|
use segment::fixtures::payload_context_fixture::create_id_tracker_fixture;
|
|
use segment::id_tracker::IdTrackerRead;
|
|
use segment::index::hnsw_index::point_scorer::BatchFilteredSearcher;
|
|
use segment::types::{Distance, Memory};
|
|
use segment::vector_storage::dense::dense_vector_storage::open_dense_vector_storage;
|
|
use segment::vector_storage::{DEFAULT_STOPPED, DenseVectorStorage, VectorStorageEnum};
|
|
use tempfile::Builder;
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
mod prof;
|
|
|
|
const DIM: usize = 1024;
|
|
|
|
fn random_vector(size: usize) -> DenseVector {
|
|
rand::make_rng::<SmallRng>()
|
|
.sample_iter(StandardUniform)
|
|
.take(size)
|
|
.collect()
|
|
}
|
|
|
|
fn random_query_batch<const SIZE: usize>() -> [QueryVector; SIZE] {
|
|
array::from_fn(|_| QueryVector::from(random_vector(DIM)))
|
|
}
|
|
|
|
fn benchmark<const IO_URING: bool, const VECTORS: usize, const BATCH: usize>(c: &mut Criterion) {
|
|
let tmp = Builder::new()
|
|
.prefix("vector-search-bench")
|
|
.tempdir()
|
|
.expect("tempdir created");
|
|
|
|
#[cfg(target_os = "linux")]
|
|
segment::vector_storage::common::set_async_scorer(IO_URING);
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
assert!(!IO_URING, "async scorer is only supported on Linux");
|
|
|
|
let mut storage = open_dense_vector_storage(tmp.path(), DIM, Distance::Dot, Memory::Cold)
|
|
.expect("vector storage created");
|
|
|
|
let mut vectors = (0..VECTORS).map(|_| {
|
|
let vector = random_vector(DIM);
|
|
(std::borrow::Cow::Owned(vector), false)
|
|
});
|
|
|
|
let result = match &mut storage {
|
|
VectorStorageEnum::DenseMemmap(v) => v.update_from(&mut vectors, &DEFAULT_STOPPED),
|
|
#[cfg(target_os = "linux")]
|
|
VectorStorageEnum::DenseUring(v) => v.update_from(&mut vectors, &DEFAULT_STOPPED),
|
|
_ => panic!("unexpected dense vector storage variant"),
|
|
};
|
|
result.expect("vector storage populated");
|
|
|
|
let id_tracker = Arc::new(AtomicRefCell::new(create_id_tracker_fixture(VECTORS)));
|
|
let id_tracker = id_tracker.borrow();
|
|
|
|
let mut group = c.benchmark_group("vector search");
|
|
|
|
let benchmark_id = format!(
|
|
"{} storage/{}k vectors/batch of {BATCH}",
|
|
if IO_URING { "io_uring" } else { "mmap" },
|
|
VECTORS / 1000,
|
|
);
|
|
|
|
group.bench_function(benchmark_id, |b| {
|
|
b.iter_batched(
|
|
random_query_batch::<BATCH>,
|
|
|vectors| {
|
|
BatchFilteredSearcher::new_for_test(
|
|
&vectors,
|
|
&storage,
|
|
id_tracker.deleted_point_bitslice(),
|
|
10,
|
|
)
|
|
.peek_top_all(&DEFAULT_STOPPED)
|
|
.expect("points scored")
|
|
},
|
|
BatchSize::SmallInput,
|
|
)
|
|
});
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
criterion_group! {
|
|
name = benches;
|
|
config = Criterion::default().with_profiler(prof::FlamegraphProfiler::new(1000));
|
|
targets =
|
|
benchmark::<false, 10_000, 1>,
|
|
benchmark::<false, 10_000, 4>,
|
|
benchmark::<false, 100_000, 1>,
|
|
benchmark::<false, 100_000, 4>,
|
|
|
|
benchmark::<true, 10_000, 1>,
|
|
benchmark::<true, 10_000, 4>,
|
|
benchmark::<true, 100_000, 1>,
|
|
benchmark::<true, 100_000, 4>,
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
|
criterion_group! {
|
|
name = benches;
|
|
config = Criterion::default().with_profiler(prof::FlamegraphProfiler::new(1000));
|
|
targets =
|
|
benchmark::<false, 10_000, 1>,
|
|
benchmark::<false, 10_000, 4>,
|
|
benchmark::<false, 100_000, 1>,
|
|
benchmark::<false, 100_000, 4>,
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
criterion_group! {
|
|
name = benches;
|
|
config = Criterion::default();
|
|
targets =
|
|
benchmark::<false, 10_000, 1>,
|
|
benchmark::<false, 10_000, 4>,
|
|
benchmark::<false, 100_000, 1>,
|
|
benchmark::<false, 100_000, 4>,
|
|
}
|
|
|
|
criterion_main!(benches);
|