blob: 4bcdaa3555c040597eded96a61d7a3e8e052ed31 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
/// Build-time serving intent used to choose DiskANN's persisted layout.
///
/// Reader I/O policy is always derived from input capabilities and measured
/// latency; this enum is not a Reader option.
pub enum DeploymentProfile {
#[default]
Auto = 0,
Memory = 1,
LocalStorage = 2,
RemoteStorage = 3,
ObjectStore = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ReadPlan {
pub window_bytes: usize,
pub graph_beam_width: usize,
pub filtered_graph_beam_width: usize,
}
impl DeploymentProfile {
pub(crate) const fn read_plan(self) -> ReadPlan {
match self {
Self::Auto | Self::LocalStorage => ReadPlan {
window_bytes: 16 * 1024,
graph_beam_width: 4,
filtered_graph_beam_width: 4,
},
Self::Memory => ReadPlan {
window_bytes: 4 * 1024,
graph_beam_width: 16,
filtered_graph_beam_width: 4,
},
Self::RemoteStorage => ReadPlan {
window_bytes: 32 * 1024,
graph_beam_width: 16,
filtered_graph_beam_width: 4,
},
Self::ObjectStore => ReadPlan {
window_bytes: 64 * 1024,
graph_beam_width: 16,
filtered_graph_beam_width: 4,
},
}
}
}
impl ReadPlan {
pub(crate) fn with_capabilities(
mut self,
capabilities: crate::io::SeekReadCapabilities,
) -> Self {
if capabilities.preferred_window_bytes > 0 {
self.window_bytes = capabilities.preferred_window_bytes;
}
// Bound pathological adapter hints while keeping enough room for one
// complete DiskANN page. The planner clips the final window to the
// section, so a non-page-sized capability hint must not split a page
// across two windows.
self.window_bytes = self.window_bytes.clamp(4 * 1024, 1024 * 1024);
self.window_bytes = self.window_bytes.div_ceil(4 * 1024) * (4 * 1024);
self.window_bytes = self.window_bytes.min(1024 * 1024);
if capabilities.max_ranges_per_pread > 0 {
self.graph_beam_width = self
.graph_beam_width
.min(capabilities.max_ranges_per_pread)
.max(1);
self.filtered_graph_beam_width = self
.filtered_graph_beam_width
.min(capabilities.max_ranges_per_pread)
.max(1);
}
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VectorIndexReaderOptions {
/// Total bytes available to required resident state and retained caches.
pub memory_budget_bytes: usize,
cache_overrides: Option<ResolvedVectorIndexReaderOptions>,
}
/// The concrete DiskANN read policy derived from input capabilities, observed
/// latency, index layout, and the Reader memory budget.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VectorIndexReadPlan {
pub random_read_latency_nanos: u64,
pub window_bytes: usize,
pub max_ranges_per_read: usize,
pub graph_beam_width: usize,
pub filtered_graph_beam_width: usize,
pub adjacency_preload_bytes: usize,
pub adjacency_cache_bytes: usize,
pub raw_vector_cache_bytes: usize,
pub memory_budget_bytes: usize,
}
impl Default for VectorIndexReaderOptions {
fn default() -> Self {
Self {
memory_budget_bytes: 4 * 1024 * 1024 * 1024,
cache_overrides: None,
}
}
}
impl VectorIndexReaderOptions {
pub fn new(memory_budget_bytes: usize) -> Self {
Self {
memory_budget_bytes,
cache_overrides: None,
}
}
pub(crate) fn resolve_cache_budgets(
self,
read_tier: DeploymentProfile,
resident_steady_bytes: usize,
adjacency_section_bytes: usize,
raw_vector_section_bytes: usize,
) -> ResolvedVectorIndexReaderOptions {
if let Some(mut overrides) = self.cache_overrides {
overrides.read_tier = read_tier;
return overrides;
}
let available = self
.memory_budget_bytes
.saturating_sub(resident_steady_bytes);
let preload_cap = match read_tier {
DeploymentProfile::Auto
| DeploymentProfile::Memory
| DeploymentProfile::LocalStorage => 16 * 1024 * 1024,
DeploymentProfile::RemoteStorage => 32 * 1024 * 1024,
DeploymentProfile::ObjectStore => 64 * 1024 * 1024,
};
let adjacency_preload_bytes = (available / 2)
.min(preload_cap)
.min(adjacency_section_bytes);
let after_preload = available.saturating_sub(adjacency_preload_bytes);
let per_cache_cap = match read_tier {
DeploymentProfile::RemoteStorage | DeploymentProfile::ObjectStore => usize::MAX,
DeploymentProfile::Auto
| DeploymentProfile::Memory
| DeploymentProfile::LocalStorage => 64 * 1024 * 1024,
};
let cold_adjacency_bytes = adjacency_section_bytes.saturating_sub(adjacency_preload_bytes);
let mut adjacency_cache_bytes = (after_preload / 2)
.min(per_cache_cap)
.min(cold_adjacency_bytes);
let mut raw_vector_cache_bytes = after_preload
.saturating_sub(adjacency_cache_bytes)
.min(per_cache_cap)
.min(raw_vector_section_bytes);
let mut unassigned = after_preload
.saturating_sub(adjacency_cache_bytes)
.saturating_sub(raw_vector_cache_bytes);
let adjacency_extra = unassigned
.min(per_cache_cap.saturating_sub(adjacency_cache_bytes))
.min(cold_adjacency_bytes.saturating_sub(adjacency_cache_bytes));
adjacency_cache_bytes = adjacency_cache_bytes.saturating_add(adjacency_extra);
unassigned = unassigned.saturating_sub(adjacency_extra);
let vector_extra = unassigned
.min(per_cache_cap.saturating_sub(raw_vector_cache_bytes))
.min(raw_vector_section_bytes.saturating_sub(raw_vector_cache_bytes));
raw_vector_cache_bytes = raw_vector_cache_bytes.saturating_add(vector_extra);
ResolvedVectorIndexReaderOptions {
read_tier,
adjacency_preload_bytes,
adjacency_cache_bytes,
max_resident_bytes: self.memory_budget_bytes,
raw_vector_cache_bytes,
}
}
#[cfg(test)]
pub(crate) fn with_cache_budgets(
read_tier: DeploymentProfile,
adjacency_preload_bytes: usize,
adjacency_cache_bytes: usize,
max_resident_bytes: usize,
raw_vector_cache_bytes: usize,
) -> Self {
Self {
memory_budget_bytes: max_resident_bytes,
cache_overrides: Some(ResolvedVectorIndexReaderOptions {
read_tier,
adjacency_preload_bytes,
adjacency_cache_bytes,
max_resident_bytes,
raw_vector_cache_bytes,
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ResolvedVectorIndexReaderOptions {
pub read_tier: DeploymentProfile,
pub adjacency_preload_bytes: usize,
pub adjacency_cache_bytes: usize,
pub max_resident_bytes: usize,
pub raw_vector_cache_bytes: usize,
}
#[cfg(test)]
mod tests {
use crate::io::SeekReadCapabilities;
use super::{DeploymentProfile, VectorIndexReaderOptions};
#[test]
fn deployment_profiles_have_distinct_read_plans() {
let options = VectorIndexReaderOptions::default();
assert_eq!(options.memory_budget_bytes, 4 * 1024 * 1024 * 1024);
let resolved = options.resolve_cache_budgets(
DeploymentProfile::Auto,
1024,
16 * 1024 * 1024,
8 * 1024 * 1024,
);
assert!(resolved.adjacency_preload_bytes > 0);
assert_eq!(
resolved.adjacency_preload_bytes + resolved.adjacency_cache_bytes,
16 * 1024 * 1024
);
assert!(resolved.raw_vector_cache_bytes > 0);
assert!(
1024 + resolved.adjacency_preload_bytes
+ resolved.adjacency_cache_bytes
+ resolved.raw_vector_cache_bytes
<= options.memory_budget_bytes
);
let memory = DeploymentProfile::Memory.read_plan();
let local = DeploymentProfile::LocalStorage.read_plan();
let remote = DeploymentProfile::RemoteStorage.read_plan();
let object_store = DeploymentProfile::ObjectStore.read_plan();
assert_eq!(
(
memory.window_bytes,
memory.graph_beam_width,
memory.filtered_graph_beam_width
),
(4096, 16, 4)
);
assert_eq!(
(
local.window_bytes,
local.graph_beam_width,
local.filtered_graph_beam_width
),
(16 * 1024, 4, 4)
);
assert_eq!(
(
remote.window_bytes,
remote.graph_beam_width,
remote.filtered_graph_beam_width
),
(32 * 1024, 16, 4)
);
assert_eq!(
(
object_store.window_bytes,
object_store.graph_beam_width,
object_store.filtered_graph_beam_width
),
(64 * 1024, 16, 4)
);
}
#[test]
fn remote_cache_budgets_use_available_memory_beyond_the_local_cache_caps() {
const MIB: usize = 1024 * 1024;
let resolved = VectorIndexReaderOptions::new(4 * 1024 * MIB).resolve_cache_budgets(
DeploymentProfile::RemoteStorage,
256 * MIB,
512 * MIB,
2 * 1024 * MIB,
);
assert_eq!(resolved.adjacency_preload_bytes, 32 * MIB);
assert_eq!(
resolved.adjacency_cache_bytes,
512 * MIB - resolved.adjacency_preload_bytes
);
assert_eq!(resolved.raw_vector_cache_bytes, 2 * 1024 * MIB);
assert!(
256 * MIB
+ resolved.adjacency_preload_bytes
+ resolved.adjacency_cache_bytes
+ resolved.raw_vector_cache_bytes
<= resolved.max_resident_bytes
);
}
#[test]
fn capability_windows_are_bounded_and_keep_complete_diskann_pages() {
let plan = DeploymentProfile::LocalStorage
.read_plan()
.with_capabilities(SeekReadCapabilities {
estimated_random_read_latency_nanos: 0,
preferred_window_bytes: 10_000,
max_ranges_per_pread: 2,
});
assert_eq!(plan.window_bytes, 12 * 1024);
assert_eq!(plan.graph_beam_width, 2);
assert_eq!(plan.filtered_graph_beam_width, 2);
let bounded = DeploymentProfile::RemoteStorage
.read_plan()
.with_capabilities(SeekReadCapabilities {
estimated_random_read_latency_nanos: 0,
preferred_window_bytes: usize::MAX,
max_ranges_per_pread: 0,
});
assert_eq!(bounded.window_bytes, 1024 * 1024);
}
}