diff --git a/docs/grpc/docs.md b/docs/grpc/docs.md index 5ed864f6bd..0bb6c6056f 100644 --- a/docs/grpc/docs.md +++ b/docs/grpc/docs.md @@ -47,6 +47,7 @@ - [ListCollectionsRequest](#qdrant-ListCollectionsRequest) - [ListCollectionsResponse](#qdrant-ListCollectionsResponse) - [LocalShardInfo](#qdrant-LocalShardInfo) + - [MaxOptimizationThreads](#qdrant-MaxOptimizationThreads) - [MoveShard](#qdrant-MoveShard) - [MultiVectorConfig](#qdrant-MultiVectorConfig) - [OptimizerStatus](#qdrant-OptimizerStatus) @@ -89,6 +90,7 @@ - [CompressionRatio](#qdrant-CompressionRatio) - [Datatype](#qdrant-Datatype) - [Distance](#qdrant-Distance) + - [MaxOptimizationThreads.Setting](#qdrant-MaxOptimizationThreads-Setting) - [Modifier](#qdrant-Modifier) - [MultiVectorComparator](#qdrant-MultiVectorComparator) - [PayloadSchemaType](#qdrant-PayloadSchemaType) @@ -1018,6 +1020,22 @@ + + +### MaxOptimizationThreads + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| value | [uint64](#uint64) | | | +| setting | [MaxOptimizationThreads.Setting](#qdrant-MaxOptimizationThreads-Setting) | | | + + + + + + ### MoveShard @@ -1101,7 +1119,8 @@ To disable vector indexing, set to `0`. Note: 1kB = 1 vector of size 256. | | flush_interval_sec | [uint64](#uint64) | optional | Interval between forced flushes. | -| max_optimization_threads | [uint64](#uint64) | optional | Max number of threads (jobs) for running optimizations per shard. Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. If null - have no limit and choose dynamically to saturate CPU. If 0 - no optimization threads, optimizations will be disabled. | +| deprecated_max_optimization_threads | [uint64](#uint64) | optional | Deprecated in favor of `max_optimization_threads` | +| max_optimization_threads | [MaxOptimizationThreads](#qdrant-MaxOptimizationThreads) | optional | Max number of threads (jobs) for running optimizations per shard. Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. If "auto" - have no limit and choose dynamically to saturate CPU. If 0 - no optimization threads, optimizations will be disabled. | @@ -1751,6 +1770,17 @@ Note: 1kB = 1 vector of size 256. | + + +### MaxOptimizationThreads.Setting + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| Auto | 0 | | + + + ### Modifier diff --git a/docs/redoc/master/openapi.json b/docs/redoc/master/openapi.json index b175e97cb2..3f05dbbeec 100644 --- a/docs/redoc/master/openapi.json +++ b/docs/redoc/master/openapi.json @@ -9449,14 +9449,36 @@ "nullable": true }, "max_optimization_threads": { - "description": "Max number of threads (jobs) for running optimizations per shard. Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. If null - have no limit and choose dynamically to saturate CPU. If 0 - no optimization threads, optimizations will be disabled.", - "type": "integer", - "format": "uint", - "minimum": 0, - "nullable": true + "description": "Max number of threads (jobs) for running optimizations per shard. Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. If \"auto\" - have no limit and choose dynamically to saturate CPU. If 0 - no optimization threads, optimizations will be disabled.", + "anyOf": [ + { + "$ref": "#/components/schemas/MaxOptimizationThreads" + }, + { + "nullable": true + } + ] } } }, + "MaxOptimizationThreads": { + "anyOf": [ + { + "$ref": "#/components/schemas/MaxOptimizationThreadsSetting" + }, + { + "type": "integer", + "format": "uint", + "minimum": 0 + } + ] + }, + "MaxOptimizationThreadsSetting": { + "type": "string", + "enum": [ + "auto" + ] + }, "InitFrom": { "description": "Operation for creating new collection and (optionally) specify index params", "type": "object", diff --git a/lib/api/src/grpc/conversions.rs b/lib/api/src/grpc/conversions.rs index a85a16501e..7a9e3ad234 100644 --- a/lib/api/src/grpc/conversions.rs +++ b/lib/api/src/grpc/conversions.rs @@ -22,10 +22,10 @@ use super::qdrant::{ raw_query, start_from, BinaryQuantization, BoolIndexParams, CompressionRatio, DatetimeIndexParams, DatetimeRange, Direction, FacetHit, FacetHitInternal, FacetValue, FacetValueInternal, FieldType, FloatIndexParams, GeoIndexParams, GeoLineString, GroupId, - HardwareUsage, HasVectorCondition, KeywordIndexParams, LookupLocation, MultiVectorComparator, - MultiVectorConfig, OrderBy, OrderValue, Range, RawVector, RecommendStrategy, RetrievedPoint, - SearchMatrixPair, SearchPointGroups, SearchPoints, ShardKeySelector, SparseIndices, StartFrom, - UuidIndexParams, VectorsOutput, WithLookup, + HardwareUsage, HasVectorCondition, KeywordIndexParams, LookupLocation, MaxOptimizationThreads, + MultiVectorComparator, MultiVectorConfig, OrderBy, OrderValue, Range, RawVector, + RecommendStrategy, RetrievedPoint, SearchMatrixPair, SearchPointGroups, SearchPoints, + ShardKeySelector, SparseIndices, StartFrom, UuidIndexParams, VectorsOutput, WithLookup, }; use crate::conversions::json; use crate::grpc::qdrant::condition::ConditionOneOf; @@ -1609,6 +1609,76 @@ impl From for StartFrom { } } +impl TryFrom for rest::MaxOptimizationThreads { + type Error = Status; + + fn try_from(value: MaxOptimizationThreads) -> Result { + use crate::grpc::qdrant::max_optimization_threads::{Setting, Variant}; + + let variant = value + .variant + .ok_or_else(|| Status::invalid_argument("Malformed MaxOptimizationThreads"))?; + + let converted = match variant { + Variant::Setting(setting_int) => { + let setting = Setting::try_from(setting_int).map_err(|err| { + Status::invalid_argument(format!( + "Invalid MaxOptimizationThreads setting: {err}" + )) + })?; + + match setting { + Setting::Auto => Self::Setting(rest::MaxOptimizationThreadsSetting::Auto), + } + } + Variant::Value(num_threads) => Self::Threads(num_threads as usize), + }; + Ok(converted) + } +} + +impl TryFrom for Option { + type Error = Status; + + fn try_from(value: MaxOptimizationThreads) -> Result { + use crate::grpc::qdrant::max_optimization_threads::{Setting, Variant}; + + let variant = value + .variant + .ok_or_else(|| Status::invalid_argument("Malformed MaxOptimizationThreads"))?; + + Ok(match variant { + Variant::Setting(setting_int) => { + let setting = Setting::try_from(setting_int).map_err(|err| { + Status::invalid_argument(format!( + "Invalid MaxOptimizationThreads setting: {err}" + )) + })?; + + match setting { + Setting::Auto => None, + } + } + Variant::Value(num_threads) => Some(num_threads as usize), + }) + } +} + +impl From> for MaxOptimizationThreads { + fn from(value: Option) -> Self { + use crate::grpc::qdrant::max_optimization_threads::{Setting, Variant}; + + let variant = match value { + None => Variant::Setting(Setting::Auto.into()), + Some(n) => Variant::Value(n as u64), + }; + + Self { + variant: Some(variant), + } + } +} + impl From for segment::types::HnswConfig { fn from(hnsw_config: HnswConfigDiff) -> Self { Self { diff --git a/lib/api/src/grpc/proto/collections.proto b/lib/api/src/grpc/proto/collections.proto index 35ca7265b4..ed5b75d7fe 100644 --- a/lib/api/src/grpc/proto/collections.proto +++ b/lib/api/src/grpc/proto/collections.proto @@ -145,6 +145,17 @@ enum CompressionRatio { x64 = 4; } +message MaxOptimizationThreads { + enum Setting { + Auto = 0; + } + + oneof variant { + uint64 value = 1; + Setting setting = 2; + } +} + message OptimizerStatus { bool ok = 1; string error = 2; @@ -260,13 +271,17 @@ message OptimizersConfigDiff { Interval between forced flushes. */ optional uint64 flush_interval_sec = 7; + + // Deprecated in favor of `max_optimization_threads` + optional uint64 deprecated_max_optimization_threads = 8; + /* Max number of threads (jobs) for running optimizations per shard. Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. - If null - have no limit and choose dynamically to saturate CPU. + If "auto" - have no limit and choose dynamically to saturate CPU. If 0 - no optimization threads, optimizations will be disabled. */ - optional uint64 max_optimization_threads = 8; + optional MaxOptimizationThreads max_optimization_threads = 9; } message ScalarQuantization { diff --git a/lib/api/src/grpc/qdrant.rs b/lib/api/src/grpc/qdrant.rs index d522598e28..b1d431f922 100644 --- a/lib/api/src/grpc/qdrant.rs +++ b/lib/api/src/grpc/qdrant.rs @@ -210,6 +210,59 @@ pub struct ListCollectionsResponse { #[derive(serde::Serialize)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct MaxOptimizationThreads { + #[prost(oneof = "max_optimization_threads::Variant", tags = "1, 2")] + pub variant: ::core::option::Option, +} +/// Nested message and enum types in `MaxOptimizationThreads`. +pub mod max_optimization_threads { + #[derive(serde::Serialize)] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum Setting { + Auto = 0, + } + impl Setting { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Setting::Auto => "Auto", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "Auto" => Some(Self::Auto), + _ => None, + } + } + } + #[derive(serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Variant { + #[prost(uint64, tag = "1")] + Value(u64), + #[prost(enumeration = "Setting", tag = "2")] + Setting(i32), + } +} +#[derive(serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct OptimizerStatus { #[prost(bool, tag = "1")] pub ok: bool, @@ -330,12 +383,15 @@ pub struct OptimizersConfigDiff { /// Interval between forced flushes. #[prost(uint64, optional, tag = "7")] pub flush_interval_sec: ::core::option::Option, + /// Deprecated in favor of `max_optimization_threads` + #[prost(uint64, optional, tag = "8")] + pub deprecated_max_optimization_threads: ::core::option::Option, /// Max number of threads (jobs) for running optimizations per shard. /// Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. - /// If null - have no limit and choose dynamically to saturate CPU. + /// If "auto" - have no limit and choose dynamically to saturate CPU. /// If 0 - no optimization threads, optimizations will be disabled. - #[prost(uint64, optional, tag = "8")] - pub max_optimization_threads: ::core::option::Option, + #[prost(message, optional, tag = "9")] + pub max_optimization_threads: ::core::option::Option, } #[derive(validator::Validate)] #[derive(serde::Serialize)] diff --git a/lib/api/src/rest/schema.rs b/lib/api/src/rest/schema.rs index 033b58b7ea..ec04ca9e72 100644 --- a/lib/api/src/rest/schema.rs +++ b/lib/api/src/rest/schema.rs @@ -1050,3 +1050,32 @@ impl PointInsertOperations { self.len() == 0 } } + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, Default, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum MaxOptimizationThreadsSetting { + #[default] + Auto, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Hash, JsonSchema)] +#[serde(untagged)] +pub enum MaxOptimizationThreads { + Setting(MaxOptimizationThreadsSetting), + Threads(usize), +} + +impl Default for MaxOptimizationThreads { + fn default() -> Self { + MaxOptimizationThreads::Setting(MaxOptimizationThreadsSetting::Auto) + } +} + +impl From for Option { + fn from(value: MaxOptimizationThreads) -> Self { + match value { + MaxOptimizationThreads::Setting(MaxOptimizationThreadsSetting::Auto) => None, + MaxOptimizationThreads::Threads(threads) => Some(threads), + } + } +} diff --git a/lib/collection/src/operations/config_diff.rs b/lib/collection/src/operations/config_diff.rs index c6b96e93ce..6b0065f310 100644 --- a/lib/collection/src/operations/config_diff.rs +++ b/lib/collection/src/operations/config_diff.rs @@ -1,6 +1,7 @@ use std::hash::Hash; use std::num::NonZeroU32; +use api::rest::MaxOptimizationThreads; use merge::Merge; use schemars::JsonSchema; use segment::types::{ @@ -162,9 +163,9 @@ pub struct OptimizersConfigDiff { pub flush_interval_sec: Option, /// Max number of threads (jobs) for running optimizations per shard. /// Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. - /// If null - have no limit and choose dynamically to saturate CPU. + /// If "auto" - have no limit and choose dynamically to saturate CPU. /// If 0 - no optimization threads, optimizations will be disabled. - pub max_optimization_threads: Option, + pub max_optimization_threads: Option, } impl std::hash::Hash for OptimizersConfigDiff { @@ -200,7 +201,29 @@ impl DiffConfig for HnswConfigDiff {} impl DiffConfig for HnswConfigDiff {} -impl DiffConfig for OptimizersConfigDiff {} +impl DiffConfig for OptimizersConfigDiff { + fn update(self, config: &OptimizersConfig) -> CollectionResult + where + Self: Sized + Serialize + DeserializeOwned + Merge, + { + Ok(OptimizersConfig { + deleted_threshold: self.deleted_threshold.unwrap_or(config.deleted_threshold), + vacuum_min_vector_number: self + .vacuum_min_vector_number + .unwrap_or(config.vacuum_min_vector_number), + default_segment_number: self + .default_segment_number + .unwrap_or(config.default_segment_number), + max_segment_size: self.max_segment_size.or(config.max_segment_size), + memmap_threshold: self.memmap_threshold.or(config.memmap_threshold), + indexing_threshold: self.indexing_threshold.or(config.indexing_threshold), + flush_interval_sec: self.flush_interval_sec.unwrap_or(config.flush_interval_sec), + max_optimization_threads: self + .max_optimization_threads + .map_or(config.max_optimization_threads, From::from), + }) + } +} impl DiffConfig for WalConfigDiff {} @@ -333,6 +356,7 @@ impl Validate for QuantizationConfigDiff { #[cfg(test)] mod tests { + use rstest::rstest; use segment::types::{Distance, HnswConfig}; use super::*; @@ -388,6 +412,31 @@ mod tests { assert_eq!(new_config.indexing_threshold, Some(10000)) } + #[rstest] + #[case::number(r#"{ "max_optimization_threads": 5 }"#, Some(5))] + #[case::auto(r#"{ "max_optimization_threads": "auto" }"#, None)] + #[case::null(r#"{ "max_optimization_threads": null }"#, Some(1))] // no effect + #[case::nothing("{ }", Some(1))] // no effect + #[should_panic] + #[case::other(r#"{ "max_optimization_threads": "other" }"#, Some(1))] + fn test_set_optimizer_threads(#[case] json_diff: &str, #[case] expected: Option) { + let base_config = OptimizersConfig { + deleted_threshold: 0.9, + vacuum_min_vector_number: 1000, + default_segment_number: 10, + max_segment_size: None, + memmap_threshold: None, + indexing_threshold: Some(50_000), + flush_interval_sec: 30, + max_optimization_threads: Some(1), + }; + + let update: OptimizersConfigDiff = serde_json::from_str(json_diff).unwrap(); + let new_config = update.update(&base_config).unwrap(); + + assert_eq!(new_config.max_optimization_threads, expected); + } + #[test] fn test_wal_config() { let base_config = WalConfig::default(); diff --git a/lib/collection/src/operations/conversions.rs b/lib/collection/src/operations/conversions.rs index 644d10c22c..c9cb921442 100644 --- a/lib/collection/src/operations/conversions.rs +++ b/lib/collection/src/operations/conversions.rs @@ -13,7 +13,7 @@ use api::grpc::qdrant::update_collection_cluster_setup_request::{ }; use api::grpc::qdrant::{CreateShardKey, Vectors}; use api::rest::schema::ShardKeySelector; -use api::rest::BaseGroupRequest; +use api::rest::{BaseGroupRequest, MaxOptimizationThreads}; use common::types::ScoreType; use itertools::Itertools; use segment::common::operation_error::OperationError; @@ -295,9 +295,11 @@ impl TryFrom for CollectionParamsDiff { } } -impl From for OptimizersConfigDiff { - fn from(value: api::grpc::qdrant::OptimizersConfigDiff) -> Self { - Self { +impl TryFrom for OptimizersConfigDiff { + type Error = Status; + + fn try_from(value: api::grpc::qdrant::OptimizersConfigDiff) -> Result { + Ok(Self { deleted_threshold: value.deleted_threshold, vacuum_min_vector_number: value.vacuum_min_vector_number.map(|v| v as usize), default_segment_number: value.default_segment_number.map(|v| v as usize), @@ -305,8 +307,15 @@ impl From for OptimizersConfigDiff { memmap_threshold: value.memmap_threshold.map(|v| v as usize), indexing_threshold: value.indexing_threshold.map(|v| v as usize), flush_interval_sec: value.flush_interval_sec, - max_optimization_threads: value.max_optimization_threads.map(|v| v as usize), - } + // TODO: remove deprecated field in a later version + max_optimization_threads: value + .deprecated_max_optimization_threads + .map(|v| MaxOptimizationThreads::Threads(v as usize)) + .or(value + .max_optimization_threads + .map(TryFrom::try_from) + .transpose()?), + }) } } @@ -426,10 +435,13 @@ impl From for api::grpc::qdrant::CollectionInfo { .indexing_threshold .map(|x| x as u64), flush_interval_sec: Some(config.optimizer_config.flush_interval_sec), - max_optimization_threads: config + deprecated_max_optimization_threads: config .optimizer_config .max_optimization_threads - .map(|n| n as u64), + .map(|x| x as u64), + max_optimization_threads: Some(From::from( + config.optimizer_config.max_optimization_threads, + )), }), wal_config: config .wal_config @@ -484,9 +496,18 @@ impl TryFrom for CollectionStatus { } } -impl From for OptimizersConfig { - fn from(optimizer_config: api::grpc::qdrant::OptimizersConfigDiff) -> Self { - Self { +impl TryFrom for OptimizersConfig { + type Error = Status; + + fn try_from( + optimizer_config: api::grpc::qdrant::OptimizersConfigDiff, + ) -> Result { + debug_assert!( + optimizer_config.max_optimization_threads.is_some(), + "This conversion is for CollectionInfo, max_optimization_threads should always have a value" + ); + + Ok(Self { deleted_threshold: optimizer_config.deleted_threshold.unwrap_or_default(), vacuum_min_vector_number: optimizer_config .vacuum_min_vector_number @@ -497,10 +518,11 @@ impl From for OptimizersConfig { memmap_threshold: optimizer_config.memmap_threshold.map(|x| x as usize), indexing_threshold: optimizer_config.indexing_threshold.map(|x| x as usize), flush_interval_sec: optimizer_config.flush_interval_sec.unwrap_or_default(), - max_optimization_threads: optimizer_config - .max_optimization_threads - .map(|n| n as usize), - } + max_optimization_threads: match optimizer_config.max_optimization_threads { + None => return Err(Status::invalid_argument("Malformed OptimizersConfig")), + Some(max_optimization_threads) => TryFrom::try_from(max_optimization_threads)?, + }, + }) } } @@ -1795,7 +1817,7 @@ impl TryFrom for CollectionConfig { }, optimizer_config: match config.optimizer_config { None => return Err(Status::invalid_argument("Malformed OptimizerConfig type")), - Some(optimizer_config) => OptimizersConfig::from(optimizer_config), + Some(optimizer_config) => OptimizersConfig::try_from(optimizer_config)?, }, wal_config: match config.wal_config { None => return Err(Status::invalid_argument("Malformed WalConfig type")), diff --git a/lib/storage/src/content_manager/conversions.rs b/lib/storage/src/content_manager/conversions.rs index 1d43c23707..737440164c 100644 --- a/lib/storage/src/content_manager/conversions.rs +++ b/lib/storage/src/content_manager/conversions.rs @@ -52,7 +52,7 @@ impl TryFrom for CollectionMetaOperations { .transpose()?, hnsw_config: value.hnsw_config.map(|v| v.into()), wal_config: value.wal_config.map(|v| v.into()), - optimizers_config: value.optimizers_config.map(|v| v.into()), + optimizers_config: value.optimizers_config.map(TryFrom::try_from).transpose()?, shard_number: value.shard_number, on_disk_payload: value.on_disk_payload, replication_factor: value.replication_factor, @@ -114,7 +114,10 @@ impl TryFrom for CollectionMetaOperations { .params .map(CollectionParamsDiff::try_from) .transpose()?, - optimizers_config: value.optimizers_config.map(OptimizersConfigDiff::from), + optimizers_config: value + .optimizers_config + .map(OptimizersConfigDiff::try_from) + .transpose()?, quantization_config: value .quantization_config .map(QuantizationConfigDiff::try_from)