Files
qdrant/lib/segment/Cargo.toml
Ivan PleshkovandClaude Fable 5 4aa8066b75 Turbo4 batched scan (#10362)
* TurboQuantizer::score_precomputed_batch: score a contiguous run of vectors

Batch counterpart of `score_precomputed` for vectors stored back to
back at `quantized_size()`: the width's kernel scores the whole run of
codes in one `dotprod_batch` call, then a second pass applies each
vector's extras.  L1 dequantizes per vector and stays a plain loop.

Tested against per-vector `score_precomputed` for every width,
distance, and mode over run lengths that leave every group remainder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* EncodedStorage::for_each_run: serve consecutive offsets as contiguous runs

`for_each_run(offsets, callback(first, count, bytes))` splits the
offsets into maximal runs of consecutive ids the storage can serve
from one contiguous slice, so a sequential scan resolves chunk lookups
and reads once per run instead of once per vector.  The default serves
every vector as its own run; `for_each_consecutive_run` is the shared
run detection for storages that override it, with a per-run cap for
chunk boundaries.  The test storage overrides it (its data is one flat
buffer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* EncodedVectors::score_points: batched scoring entry point, run-batched for TQ

`score_points(query, offsets, scores)` scores a batch of points.  The
default keeps the per-vector loop the scorers run today, so SQ/PQ/BQ
are unchanged.  TurboQuant overrides it: on RAM/mmap storages it walks
`for_each_run` and scores each contiguous run with one
`score_precomputed_batch` call, hoisting the score inversion out of
the loop; backends with async reads keep the pipelined per-vector
path.  Non-consecutive offsets degrade to single-vector runs, so
scattered access keeps its previous cost.

Integration test: `score_points` vs `score_point` for every bit width
and mode, Dot and inverted L2, over sequential, scattered and
descending id orders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Quantized storages: for_each_run over their contiguous regions

The RAM storage and both chunked mmap storages cap runs at their chunk
boundary and serve each run with one `get_many`; the single-file mmap
storage serves any run as one sequential read.  Unit test on the RAM
storage: runs cover every offset once, in order, with bytes identical
to per-point reads, across the internal chunk boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* QuantizedQueryScorer: score batches through EncodedVectors::score_points

Routes `score_stored_batch` through the batched entry point, so
TurboQuant-as-quantization scans score contiguous runs with one kernel
call per run; SQ/PQ/BQ keep the per-vector loop via the default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* TurboScoring::score_query_batch: run-batched scoring for Turbo4 storages

Adds the batch counterpart of `score_query_bytes` to the trait, with
one shared implementation over the storage's `EncodedStorage`:
consecutive ids are coalesced into contiguous runs, each run scored by
a single `score_precomputed_batch` call, and the metric sign applied
once over the batch.  Backends with async reads keep the pipelined
per-vector path.  `TurboQueryScorer::score_stored_batch` now calls it.

The batch-vs-single storage test grows to 8192 vectors so a full
ascending scan crosses a chunk boundary of the chunked backend, and
runs that scan on the chunked, mmap and io_uring backends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* score_precomputed_batch: keep the extras pass in L1

The kernel pass and the extras pass now alternate over sub-runs of 64
vectors instead of each covering the whole run: for a run of several
hundred vectors the second pass otherwise refetched every vector's
extras from L2.  Measured with 512-vector runs from the full-scan
driver at dim 512: the regression against 64-vector runs went from
+11 % to +2 %.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Bench: exhaustive search over Turbo4 storages through the plain-index driver

`turbo4_full_scan` runs `BatchFilteredSearcher::peek_top_visible` —
the exact path of a non-indexed search — over 200k normalized random
vectors for Turbo4 as datatype (appendable chunked, in RAM) and Turbo4
as quantization (over a RAM dense storage), at dims 64 to 1024, so the
fixed per-point cost of the scan driver is measured next to the kernel.
`TURBO_SCAN_DIMS=64,128` narrows the dims while iterating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 11:41:07 +02:00

236 lines
5.3 KiB
TOML

[package]
name = "segment"
version = "0.6.0"
authors = [
"Andrey Vasnetsov <vasnetsov93@gmail.com>",
"Qdrant Team <info@qdrant.tech>",
]
license = "Apache-2.0"
edition = "2024"
[lints]
workspace = true
[features]
default = []
testing = ["common/testing", "sparse/testing", "gpu/testing", "quantization/testing"]
gpu = ["gpu/gpu"]
[build-dependencies]
cc = { workspace = true }
[dev-dependencies]
clap = { workspace = true }
criterion = { workspace = true }
dataset = { path = "../common/dataset" }
env_logger = { workspace = true }
fs-err = { workspace = true, features = ["debug"] }
humantime = { workspace = true }
indicatif = { workspace = true }
ndarray = "0.17.2"
ndarray-npy = { version = "0.10.0", default-features = false }
rmp-serde = { workspace = true }
rand_distr = { workspace = true }
walkdir = { workspace = true }
rstest = { workspace = true }
segment = { path = ".", default-features = false, features = ["testing"] }
proptest = { workspace = true }
anyhow = { workspace = true }
object_store = { workspace = true }
bytes = { workspace = true }
tokio = { workspace = true }
tap = { workspace = true }
[target.'cfg(not(target_os = "windows"))'.dev-dependencies]
pprof = { workspace = true }
[dependencies]
io_bridge_object_store = { path = "../common/io_bridge_object_store" }
blink-alloc = { workspace = true }
futures = { workspace = true }
bytemuck = { workspace = true }
data-encoding = { workspace = true }
fs-err = { workspace = true }
integer-encoding = { workspace = true }
tempfile = { workspace = true }
parking_lot = { workspace = true }
rayon = { workspace = true }
itertools = { workspace = true }
uuid = { workspace = true }
bincode = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_cbor = { workspace = true }
serde-value = "0.7"
serde_variant = { workspace = true }
serde-untagged = "0.1.9"
ordered-float = { workspace = true }
thiserror = { workspace = true }
atomic_refcell = { workspace = true }
atomicwrites = { workspace = true }
memmap2 = { workspace = true }
schemars = { workspace = true }
log = { workspace = true }
geo = { version = "0.33.1", default-features = false }
geohash = "0.13.2"
num-traits = { workspace = true }
num-derive = "0.5.1"
num-cmp = "0.1.0"
once_cell = "1.21"
rand = { workspace = true }
bitvec = { workspace = true }
fs_extra = { workspace = true }
tinyvec = { workspace = true }
validator = { workspace = true }
chrono = { workspace = true }
ecow = { workspace = true }
fnv = { workspace = true }
indexmap = { workspace = true }
ahash = { workspace = true }
self_cell.workspace = true
sha2 = { workspace = true }
siphasher = "1.0.3"
smallvec = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true }
byteorder = { workspace = true }
stumpalo = { workspace = true }
zerocopy = { workspace = true }
vaporetto = { version = "0.6.5" }
rust-stemmers = { package = "qdrant-rust-stemmers", version = "1.2.2" }
# Memory only; disable `user` so macOS does not pull objc2 via open-directory
# (objc2::Retained's IntoIterator blanket impl overflows trait resolution).
sysinfo = { workspace = true }
charabia = { version = "0.10.0", default-features = false, features = [
"greek",
"hebrew",
"thai",
"chinese-segmentation",
"chinese-normalization",
] }
blobstore = { path = "../blobstore" }
common = { path = "../common/common" }
macros = { path = "../macros" }
posting_list = { path = "../posting_list" }
quantization = { path = "../quantization" }
sparse = { path = "../sparse" }
gpu = { path = "../gpu" }
tracing = { workspace = true, optional = true }
macro_rules_attribute = "0.2.2"
nom = "8.0.0"
half = { workspace = true }
roaring = { workspace = true }
duplicate = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
procfs = { version = "0.18", default-features = false }
[[bench]]
name = "vector_search"
harness = false
[[bench]]
name = "turbo_vector_search"
harness = false
[[bench]]
name = "turbo_full_scan"
harness = false
[[bench]]
name = "read_vectors"
harness = false
[[bench]]
name = "read_payloads"
harness = false
[[bench]]
name = "hnsw_build_graph"
harness = false
[[bench]]
name = "hnsw_search_graph"
harness = false
[[bench]]
name = "conditional_search"
harness = false
[[bench]]
name = "dynamic_mmap_flags"
harness = false
[[bench]]
name = "hnsw_build_asymptotic"
harness = false
[[bench]]
name = "serde_formats"
harness = false
[[bench]]
name = "id_type_benchmark"
harness = false
[[bench]]
name = "map_benchmark"
harness = false
[[bench]]
name = "boolean_filtering"
harness = false
[[bench]]
name = "numeric_index_check_values"
harness = false
[[bench]]
name = "sparse_index_search"
harness = false
[[bench]]
name = "sparse_index_build"
harness = false
bench = false # TODO(rocksdb): fix and re-enable (or just remove)
[[bench]]
name = "sparse_vector_storage"
harness = false
bench = false # TODO(rocksdb): fix and re-enable (or just remove)
[[bench]]
name = "multi_vector_search"
harness = false
[[bench]]
name = "metrics"
harness = false
[[bench]]
name = "range_filtering"
harness = false
[[bench]]
name = "in_memory_id_tracker"
harness = false
[[bench]]
name = "buffered_update_bitslice"
harness = false
[[bench]]
name = "segment_info"
harness = false
[[bench]]
name = "hnsw_incremental_build"
harness = false
[[bench]]
name = "facets"
harness = false