diff options
| -rw-r--r-- | ydb/core/kqp/compute_actor/kqp_compute_actor.cpp | 5 | ||||
| -rw-r--r-- | ydb/core/kqp/compute_actor/kqp_compute_actor.h | 4 | ||||
| -rw-r--r-- | ydb/core/kqp/proxy_service/kqp_proxy_service.cpp | 8 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_stream_lookup_actor.cpp | 25 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_stream_lookup_actor.h | 4 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_stream_lookup_factory.cpp | 10 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_stream_lookup_factory.h | 4 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_stream_lookup_worker.cpp | 189 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_stream_lookup_worker.h | 4 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_vector_index_levels_cache.cpp | 158 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/kqp_vector_index_levels_cache.h | 216 | ||||
| -rw-r--r-- | ydb/core/kqp/runtime/ya.make | 4 | ||||
| -rw-r--r-- | ydb/core/kqp/ut/indexes/vector/kqp_indexes_vector_ut.cpp | 238 | ||||
| -rw-r--r-- | ydb/core/protos/table_service_config.proto | 3 |
14 files changed, 796 insertions, 76 deletions
diff --git a/ydb/core/kqp/compute_actor/kqp_compute_actor.cpp b/ydb/core/kqp/compute_actor/kqp_compute_actor.cpp index 277e1d29633..0accc33a3eb 100644 --- a/ydb/core/kqp/compute_actor/kqp_compute_actor.cpp +++ b/ydb/core/kqp/compute_actor/kqp_compute_actor.cpp @@ -165,10 +165,11 @@ IDqOutputConsumer::TPtr TKqpTaskRunnerExecutionContext::CreateOutputConsumer(con NYql::NDq::IDqAsyncIoFactory::TPtr CreateKqpAsyncIoFactory( TIntrusivePtr<TKqpCounters> counters, std::optional<TKqpFederatedQuerySetup> federatedQuerySetup, - std::shared_ptr<NYql::NDq::IS3ActorsFactory> s3ActorsFactory + std::shared_ptr<NYql::NDq::IS3ActorsFactory> s3ActorsFactory, + TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache ) { auto factory = MakeIntrusive<NYql::NDq::TDqAsyncIoFactory>(); - RegisterStreamLookupActorFactory(*factory, counters); + RegisterStreamLookupActorFactory(*factory, counters, std::move(vectorIndexLevelsCache)); RegisterKqpReadActor(*factory, counters); RegisterKqpWriteActor(*factory, counters); RegisterSequencerActorFactory(*factory, counters); diff --git a/ydb/core/kqp/compute_actor/kqp_compute_actor.h b/ydb/core/kqp/compute_actor/kqp_compute_actor.h index 18946047797..28cf272388d 100644 --- a/ydb/core/kqp/compute_actor/kqp_compute_actor.h +++ b/ydb/core/kqp/compute_actor/kqp_compute_actor.h @@ -3,6 +3,7 @@ #include <ydb/core/kqp/compute_actor/kqp_compute_events.h> #include <ydb/core/kqp/counters/kqp_counters.h> #include <ydb/core/kqp/federated_query/kqp_federated_query_helpers.h> +#include <ydb/core/kqp/runtime/kqp_vector_index_levels_cache.h> #include <ydb/core/kqp/runtime/scheduler/kqp_schedulable_actor.h> #include <ydb/core/scheme/scheme_tabledefs.h> #include <ydb/library/yql/dq/actors/compute/dq_compute_actor.h> @@ -89,7 +90,8 @@ IActor* CreateKqpScanFetcher(const NKikimrKqp::TKqpSnapshot& snapshot, std::vect NYql::NDq::IDqAsyncIoFactory::TPtr CreateKqpAsyncIoFactory( TIntrusivePtr<TKqpCounters> counters, std::optional<TKqpFederatedQuerySetup> federatedQuerySetup, - std::shared_ptr<NYql::NDq::IS3ActorsFactory> s3ActorsFactory); + std::shared_ptr<NYql::NDq::IS3ActorsFactory> s3ActorsFactory, + TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache); } // namespace NKqp } // namespace NKikimr diff --git a/ydb/core/kqp/proxy_service/kqp_proxy_service.cpp b/ydb/core/kqp/proxy_service/kqp_proxy_service.cpp index 4c953d49bdc..9695af9ea3b 100644 --- a/ydb/core/kqp/proxy_service/kqp_proxy_service.cpp +++ b/ydb/core/kqp/proxy_service/kqp_proxy_service.cpp @@ -4,6 +4,7 @@ #include <ydb/core/actorlib_impl/long_timer.h> #include <ydb/core/base/appdata.h> +#include <ydb/core/base/counters.h> #include <ydb/core/base/feature_flags.h> #include <ydb/core/base/location.h> #include <ydb/core/base/path.h> @@ -224,11 +225,15 @@ public: void Bootstrap(const TActorContext &ctx) { NLwTraceMonPage::ProbeRegistry().AddProbesList(LWTRACE_GET_PROBES(KQP_PROVIDER)); Counters = MakeIntrusive<TKqpCounters>(AppData()->Counters, &TlsActivationContext->AsActorContext()); + VectorIndexLevelsCache = MakeIntrusive<TVectorIndexLevelsCache>( + GetServiceCounters(AppData()->Counters, "kqp")); + ctx.Register(CreateVectorIndexLevelsCacheMaintainer( + VectorIndexLevelsCache, GetKqpResourceManager(), TableServiceConfig.GetResourceManager())); FeatureFlags = AppData()->FeatureFlags; WorkloadManagerConfig = AppData()->WorkloadManagerConfig; // NOTE: some important actors are constructed within next call FederatedQuerySetup = FederatedQuerySetupFactory->Make(ctx.ActorSystem()); - AsyncIoFactory = CreateKqpAsyncIoFactory(Counters, FederatedQuerySetup, S3ActorsFactory); + AsyncIoFactory = CreateKqpAsyncIoFactory(Counters, FederatedQuerySetup, S3ActorsFactory, VectorIndexLevelsCache); ModuleResolverState = MakeIntrusive<TModuleResolverState>(); LocalSessions = std::make_unique<TLocalSessionsRegistry>(AppData()->RandomProvider); @@ -2022,6 +2027,7 @@ private: TIntrusivePtr<TExecuterMutableConfig> ExecuterConfig; TIntrusivePtr<TKqpCounters> Counters; + TIntrusivePtr<TVectorIndexLevelsCache> VectorIndexLevelsCache; std::unique_ptr<TLocalSessionsRegistry> LocalSessions; std::shared_ptr<TKqpProxySharedResources> KqpProxySharedResources; std::shared_ptr<NYql::NDq::IS3ActorsFactory> S3ActorsFactory; diff --git a/ydb/core/kqp/runtime/kqp_stream_lookup_actor.cpp b/ydb/core/kqp/runtime/kqp_stream_lookup_actor.cpp index 64630ae6d83..356d8117a73 100644 --- a/ydb/core/kqp/runtime/kqp_stream_lookup_actor.cpp +++ b/ydb/core/kqp/runtime/kqp_stream_lookup_actor.cpp @@ -65,7 +65,7 @@ TKqpStreamLockSettings BuildStreamLockSettings( class TKqpStreamLookupActor : public NActors::TActorBootstrapped<TKqpStreamLookupActor>, public NYql::NDq::IDqComputeActorAsyncInput { public: TKqpStreamLookupActor(NYql::NDq::IDqAsyncIoFactory::TInputTransformArguments&& args, NKikimrKqp::TKqpStreamLookupSettings&& settings, - TIntrusivePtr<TKqpCounters> counters) + TIntrusivePtr<TKqpCounters> counters, TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache) : LogPrefix(TStringBuilder() << "StreamLookupActor, inputIndex: " << args.InputIndex << ", CA Id " << args.ComputeActorId) , InputIndex(args.InputIndex) , Input(args.TransformInput) @@ -97,12 +97,13 @@ public: args.TaskId, args.TypeEnv, args.HolderFactory, - args.InputDesc)) + args.InputDesc, vectorIndexLevelsCache)) , MaxTotalBytesQuota(MaxTotalBytesQuotaStreamLookup()) , MaxRowsProcessing(MaxRowsProcessingStreamLookup()) , MaxInFlightReads(MaxInFlightReadsStreamLookup()) , MaxBytesPerFetch(MaxBytesPerFetchStreamLookup()) , Counters(counters) + , VectorIndexLevelsCache(std::move(vectorIndexLevelsCache)) , LookupActorSpan(TWilsonKqp::LookupActor, std::move(args.TraceId), "LookupActor") { IngressStats.Level = args.StatsLevel; @@ -435,7 +436,7 @@ private: void PassAway() final { Counters->StreamLookupActorsCount->Dec(); - + if (!LockSendTime.empty()) { TInstant now = AppData()->TimeProvider->Now(); TDuration maxInFlightTime = TDuration::Zero(); @@ -447,7 +448,7 @@ private: } Counters->MaxInFlightLockTimeOnExit->Collect(maxInFlightTime.MilliSeconds()); } - + { auto alloc = BindAllocator(); Input.Clear(); @@ -947,7 +948,7 @@ private: auto lockState = TLockState(requestId, shardId); lockState.State = TLockState::EState::Running; Reads.insertLock(requestId, std::move(lockState)); - + LockSendTime[requestId] = AppData()->TimeProvider->Now(); } @@ -958,7 +959,7 @@ private: << ", status: " << record.GetStatus()); ui64 requestId = record.GetRequestId(); - + if (auto it = LockSendTime.find(requestId); it != LockSendTime.end()) { Counters->LockLatencyHistogram->Collect((AppData()->TimeProvider->Now() - it->second).MilliSeconds()); LockSendTime.erase(it); @@ -1059,7 +1060,7 @@ private: if (hasModifiedRows) { ProcessInputRows(); } - + if (hasUnmodifiedRows) { Send(ComputeActorId, new TEvNewAsyncInputDataArrived(InputIndex)); } @@ -1302,6 +1303,7 @@ private: const ui64 QuerySpanId; TReads Reads; bool SentResultsAvailable = false; + THashMap<ui64, TString> CacheKeys; NUdf::EFetchStatus LastFetchStatus = NUdf::EFetchStatus::Yield; TPartitioning::TCPtr Partitioning; const TDuration SchemeCacheRequestTimeout; @@ -1333,17 +1335,20 @@ private: size_t MaxRowsDefaultQuota = 0; TIntrusivePtr<TKqpCounters> Counters; + TIntrusivePtr<TVectorIndexLevelsCache> VectorIndexLevelsCache; + NWilson::TSpan LookupActorSpan; NWilson::TSpan LookupActorStateSpan; - + THashMap<ui64, TInstant> LockSendTime; }; } // namespace std::pair<NYql::NDq::IDqComputeActorAsyncInput*, NActors::IActor*> CreateStreamLookupActor(NYql::NDq::IDqAsyncIoFactory::TInputTransformArguments&& args, - NKikimrKqp::TKqpStreamLookupSettings&& settings, TIntrusivePtr<TKqpCounters> counters) { - auto actor = new TKqpStreamLookupActor(std::move(args), std::move(settings), counters); + NKikimrKqp::TKqpStreamLookupSettings&& settings, TIntrusivePtr<TKqpCounters> counters, + TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache) { + auto actor = new TKqpStreamLookupActor(std::move(args), std::move(settings), counters, std::move(vectorIndexLevelsCache)); return {actor, actor}; } diff --git a/ydb/core/kqp/runtime/kqp_stream_lookup_actor.h b/ydb/core/kqp/runtime/kqp_stream_lookup_actor.h index 78839d1f2d9..06a5ba0973a 100644 --- a/ydb/core/kqp/runtime/kqp_stream_lookup_actor.h +++ b/ydb/core/kqp/runtime/kqp_stream_lookup_actor.h @@ -1,6 +1,7 @@ #pragma once #include <ydb/core/kqp/counters/kqp_counters.h> +#include <ydb/core/kqp/runtime/kqp_vector_index_levels_cache.h> #include <ydb/library/yql/dq/actors/compute/dq_compute_actor_async_io.h> #include <ydb/core/protos/kqp.pb.h> @@ -8,7 +9,8 @@ namespace NKikimr { namespace NKqp { std::pair<NYql::NDq::IDqComputeActorAsyncInput*, NActors::IActor*> CreateStreamLookupActor(NYql::NDq::IDqAsyncIoFactory::TInputTransformArguments&& args, - NKikimrKqp::TKqpStreamLookupSettings&& settings, TIntrusivePtr<TKqpCounters>); + NKikimrKqp::TKqpStreamLookupSettings&& settings, TIntrusivePtr<TKqpCounters>, + TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache); void InterceptStreamLookupActorPipeCache(NActors::TActorId); diff --git a/ydb/core/kqp/runtime/kqp_stream_lookup_factory.cpp b/ydb/core/kqp/runtime/kqp_stream_lookup_factory.cpp index d244303fd95..d9b552c248e 100644 --- a/ydb/core/kqp/runtime/kqp_stream_lookup_factory.cpp +++ b/ydb/core/kqp/runtime/kqp_stream_lookup_factory.cpp @@ -4,10 +4,12 @@ namespace NKikimr { namespace NKqp { -void RegisterStreamLookupActorFactory(NYql::NDq::TDqAsyncIoFactory& factory, TIntrusivePtr<TKqpCounters> counters) { - factory.RegisterInputTransform<NKikimrKqp::TKqpStreamLookupSettings>("StreamLookupInputTransformer", [counters](NKikimrKqp::TKqpStreamLookupSettings&& settings, - NYql::NDq::TDqAsyncIoFactory::TInputTransformArguments&& args) { - return CreateStreamLookupActor(std::move(args), std::move(settings), counters); +void RegisterStreamLookupActorFactory(NYql::NDq::TDqAsyncIoFactory& factory, TIntrusivePtr<TKqpCounters> counters, + TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache) { + factory.RegisterInputTransform<NKikimrKqp::TKqpStreamLookupSettings>("StreamLookupInputTransformer", + [counters, vectorIndexLevelsCache](NKikimrKqp::TKqpStreamLookupSettings&& settings, + NYql::NDq::TDqAsyncIoFactory::TInputTransformArguments&& args) { + return CreateStreamLookupActor(std::move(args), std::move(settings), counters, vectorIndexLevelsCache); }); } diff --git a/ydb/core/kqp/runtime/kqp_stream_lookup_factory.h b/ydb/core/kqp/runtime/kqp_stream_lookup_factory.h index db49e005fa9..70e6fe019aa 100644 --- a/ydb/core/kqp/runtime/kqp_stream_lookup_factory.h +++ b/ydb/core/kqp/runtime/kqp_stream_lookup_factory.h @@ -1,12 +1,14 @@ #pragma once #include <ydb/core/kqp/counters/kqp_counters.h> +#include <ydb/core/kqp/runtime/kqp_vector_index_levels_cache.h> #include <ydb/library/yql/dq/actors/compute/dq_compute_actor_async_io_factory.h> namespace NKikimr { namespace NKqp { -void RegisterStreamLookupActorFactory(NYql::NDq::TDqAsyncIoFactory& factory, TIntrusivePtr<NKqp::TKqpCounters>); +void RegisterStreamLookupActorFactory(NYql::NDq::TDqAsyncIoFactory& factory, TIntrusivePtr<NKqp::TKqpCounters>, + TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache); } // namespace NKqp } // namespace NKikimr diff --git a/ydb/core/kqp/runtime/kqp_stream_lookup_worker.cpp b/ydb/core/kqp/runtime/kqp_stream_lookup_worker.cpp index c44471b33be..39b08617ed8 100644 --- a/ydb/core/kqp/runtime/kqp_stream_lookup_worker.cpp +++ b/ydb/core/kqp/runtime/kqp_stream_lookup_worker.cpp @@ -1,5 +1,6 @@ #include "kqp_stream_lookup_worker.h" #include "kqp_stream_lookup_join_helpers.h" +#include "kqp_vector_index_levels_cache.h" #include <ydb/core/kqp/common/kqp_resolve.h> #include <ydb/core/kqp/common/kqp_types.h> @@ -42,9 +43,19 @@ TStreamLookupShardReadResult::~TStreamLookupShardReadResult() { namespace { +// Check if this is a vector index level table +bool IsVectorIndexLevelTable(const TString& tablePath) { + return tablePath.EndsWith(NTableIndex::NKMeans::LevelTable); +} + +struct TCacheData { + ui64 PendingReads = 0; + TOwnedCellVecBatch Batch; +}; struct TReadState { std::vector<TOwnedTableRange> PendingKeys; + std::optional<TString> CacheKey; TMaybe<TOwnedCellVec> LastProcessedKey; ui32 FirstUnprocessedQuery = 0; @@ -106,8 +117,12 @@ TTableId TKqpStreamLookupWorker::GetTableId() const { class TKqpLookupRows : public TKqpStreamLookupWorker { public: TKqpLookupRows(TLookupSettings&& settings, const NMiniKQL::TTypeEnvironment& typeEnv, - const NMiniKQL::THolderFactory& holderFactory) + const NMiniKQL::THolderFactory& holderFactory, TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache) : TKqpStreamLookupWorker(std::move(settings), typeEnv, holderFactory) + , VectorIndexLevelsCache( + IsVectorIndexLevelTable(Settings.TablePath) && vectorIndexLevelsCache && vectorIndexLevelsCache->MaxBytes() > 0 + ? std::move(vectorIndexLevelsCache) + : nullptr) { } @@ -145,7 +160,7 @@ public: const i32 lookupKeySize = std::min(Settings.KeyColumns.size(), Settings.InputColumns.size()); NMiniKQL::TStringProviderBackend backend; std::vector<TCell> keyCells(lookupKeySize); - + for (size_t colId = 0; colId < Settings.InputColumns.size(); ++colId) { const auto& lookupKeyColumn = Settings.InputColumns[colId]; if (0 <= lookupKeyColumn.KeyOrder) { @@ -160,6 +175,16 @@ public: } virtual void AddInputRowImpl(std::vector<TCell> keyCells) { + std::optional<TString> cacheKey; + if (VectorIndexLevelsCache) { + cacheKey = TSerializedCellVec::Serialize(keyCells); + auto result = VectorIndexLevelsCache->Get(Settings.TableId.PathId, cacheKey.value()); + if (result && !result->BatchRows.empty()) { + PendingBatches.push_back(result); + return; + } + } + if (keyCells.size() < Settings.KeyColumns.size()) { // build prefix range [[key_prefix, NULL, ..., NULL], [key_prefix, +inf, ..., +inf]) std::vector<TCell> fromCells(Settings.KeyColumns.size()); @@ -175,6 +200,10 @@ public: // full pk, build point UnprocessedKeys.emplace_back(std::move(keyCells)); } + + if (cacheKey.has_value()) { + CacheKeys.emplace_back(std::move(cacheKey.value())); + } } void RebuildRequest(const ui64 shardId, const ui64& prevReadId, ui64& newReadId) final { @@ -259,6 +288,28 @@ public: UnprocessedKeys.pop_front(); auto partitions = partitioning->GetIntersectionWithRange(GetKeyColumnTypes(), range); + if (!CacheKeys.empty()) { + ui64 pendingReads = 0; + for(auto [shardId, range] : partitions) { + THolder<TEvDataShard::TEvRead> request(new TEvDataShard::TEvRead()); + std::vector<TOwnedTableRange> ranges = {range}; + ui64 nextReadId = ++readId; + FillReadRequest(nextReadId, request, ranges); + ScheduledReads.emplace_back(shardId, std::move(request)); + ++pendingReads; + YQL_ENSURE(ReadStateByReadId.emplace( + nextReadId, + TReadState{ + .PendingKeys = std::move(ranges), + .CacheKey = CacheKeys.front(), + }).second); + } + + CacheKeysMap[CacheKeys.front()] = {.PendingReads = pendingReads}; + CacheKeys.pop_front(); + continue; + } + for (auto [shardId, range] : partitions) { if (range.Point) { pointsPerShard[shardId].push_back(std::move(range)); @@ -292,7 +343,7 @@ public: } bool HasPendingResults() final { - return !ReadResults.empty(); + return !ReadResults.empty() || !PendingBatches.empty(); } void AddResult(TStreamLookupShardReadResult result) final { @@ -309,10 +360,66 @@ public: ReadResults.emplace_back(std::move(result)); } + void ProcessResultRow(NKikimr::NMiniKQL::TUnboxedValueBatch& batch, TConstArrayRef<TCell> resultRow, TReadResultStats& resultStats, ui64 shardId, bool hasReadStats) { + YQL_ENSURE(resultRow.size() <= Settings.Columns.size(), "Result columns mismatch"); + + if (Settings.VectorTopK && Settings.VectorTopK->DistinctColumnsSize()) { + TVector<TCell> uniqueKey; + + for (auto& colIdx: Settings.VectorTopK->GetDistinctColumns()) { + YQL_ENSURE(colIdx < resultRow.size(), "Unique column index is too large"); + uniqueKey.push_back(resultRow.at(colIdx)); + } + + TString serializedKey = TSerializedCellVec::Serialize(uniqueKey); + + if (UniqueKeys.contains(serializedKey)) { + return; + } + UniqueKeys.insert(serializedKey); + } + + NUdf::TUnboxedValue* rowItems = nullptr; + auto row = HolderFactory.CreateDirectArrayHolder(Settings.Columns.size(), rowItems); + i64 storageRowSize = 0; + for (size_t colIndex = 0, resultColIndex = 0; colIndex < Settings.Columns.size(); ++colIndex) { + const auto& column = Settings.Columns[colIndex]; + if (IsSystemColumn(column.Name)) { + NMiniKQL::FillSystemColumn(rowItems[colIndex], shardId, column.Id, column.PType); + } else { + YQL_ENSURE(resultColIndex < resultRow.size()); + storageRowSize += resultRow[resultColIndex].Size(); + rowItems[colIndex] = NMiniKQL::GetCellValue(resultRow[resultColIndex], column.PType); + ++resultColIndex; + } + } + + batch.push_back(std::move(row)); + storageRowSize = std::max(storageRowSize, (i64)8); + + + if (!Settings.VectorTopK || !hasReadStats) { + resultStats.ReadRowsCount += 1; + resultStats.ReadBytesCount += storageRowSize; + } + + resultStats.ResultRowsCount += 1; + resultStats.ResultBytesCount += storageRowSize; + } + TReadResultStats ReplyResult(NKikimr::NMiniKQL::TUnboxedValueBatch& batch, i64 ) final { TReadResultStats resultStats; batch.clear(); + while(!PendingBatches.empty()) { + auto& frontline = PendingBatches.front(); + for(const auto& row: frontline->BatchRows) { + ProcessResultRow(batch, row, resultStats, 0, false); + } + + PendingBatches.pop_front(); + } + while (!ReadResults.empty() && !resultStats.SizeLimitExceeded) { auto& result = ReadResults.front(); if (!result.UnprocessedResultRow && Settings.VectorTopK && @@ -322,52 +429,30 @@ public: } for (; result.UnprocessedResultRow < result.ReadResult->Get()->GetRowsCount(); ++result.UnprocessedResultRow) { const auto& resultRow = result.ReadResult->Get()->GetCells(result.UnprocessedResultRow); - YQL_ENSURE(resultRow.size() <= Settings.Columns.size(), "Result columns mismatch"); - - if (Settings.VectorTopK && Settings.VectorTopK->DistinctColumnsSize()) { - TVector<TCell> uniqueKey; - for (auto& colIdx: Settings.VectorTopK->GetDistinctColumns()) { - YQL_ENSURE(colIdx < resultRow.size(), "Unique column index is too large"); - uniqueKey.push_back(resultRow.at(colIdx)); - } - TString serializedKey = TSerializedCellVec::Serialize(uniqueKey); - if (UniqueKeys.contains(serializedKey)) { - continue; - } - UniqueKeys.insert(serializedKey); - } - - NUdf::TUnboxedValue* rowItems = nullptr; - auto row = HolderFactory.CreateDirectArrayHolder(Settings.Columns.size(), rowItems); - - i64 storageRowSize = 0; - for (size_t colIndex = 0, resultColIndex = 0; colIndex < Settings.Columns.size(); ++colIndex) { - const auto& column = Settings.Columns[colIndex]; - if (IsSystemColumn(column.Name)) { - NMiniKQL::FillSystemColumn(rowItems[colIndex], result.ShardId, column.Id, column.PType); - } else { - YQL_ENSURE(resultColIndex < resultRow.size()); - storageRowSize += resultRow[resultColIndex].Size(); - rowItems[colIndex] = NMiniKQL::GetCellValue(resultRow[resultColIndex], column.PType); - ++resultColIndex; - } - } - - batch.push_back(std::move(row)); - storageRowSize = std::max(storageRowSize, (i64)8); - - if (!Settings.VectorTopK || !result.ReadResult->Get()->Record.HasStats()) { - resultStats.ReadRowsCount += 1; - resultStats.ReadBytesCount += storageRowSize; - } - resultStats.ResultRowsCount += 1; - resultStats.ResultBytesCount += storageRowSize; + ProcessResultRow(batch, resultRow, resultStats, result.ShardId, result.ReadResult->Get()->Record.HasStats()); } if (result.UnprocessedResultRow == result.ReadResult->Get()->GetRowsCount()) { if (result.ReadResult->Get()->Record.GetFinished()) { // delete finished read auto it = ReadStateByReadId.find(result.ReadResult->Get()->Record.GetReadId()); + if (it->second.CacheKey.has_value()) { + auto entryIt = CacheKeysMap.find(it->second.CacheKey.value()); + YQL_ENSURE(entryIt != CacheKeysMap.end()); + auto& entry = entryIt->second; + entry.PendingReads--; + for(ui32 idx = 0; idx < result.ReadResult->Get()->GetRowsCount(); idx++) { + entry.Batch.Append(result.ReadResult->Get()->GetCells(idx)); + } + + if (entry.PendingReads == 0) { + auto ptr = MakeIntrusive<TCachedLevelTableData>(); + ptr->BatchRows = std::move(entry.Batch); + VectorIndexLevelsCache->Put(Settings.TableId.PathId, it->second.CacheKey.value(), ptr); + CacheKeysMap.erase(entryIt); + } + } + ReadStateByReadId.erase(it); } @@ -417,7 +502,8 @@ public: return UnprocessedKeys.empty() && ReadStateByReadId.empty() && ReadResults.empty() - && ScheduledReads.empty(); + && ScheduledReads.empty() + && PendingBatches.empty(); } void ResetRowsProcessing(ui64 readId) final { @@ -463,7 +549,7 @@ private: } } - if (Settings.VectorTopK) { + if (Settings.VectorTopK && !VectorIndexLevelsCache) { *record.MutableVectorTopK() = *Settings.VectorTopK; } @@ -495,11 +581,15 @@ private: } private: + std::unordered_map<TString, TCacheData> CacheKeysMap; + std::deque<TString> CacheKeys; std::deque<TOwnedTableRange> UnprocessedKeys; std::deque<std::pair<ui64, THolder<TEvDataShard::TEvRead>>> ScheduledReads; std::unordered_map<ui64, TReadState> ReadStateByReadId; std::deque<TStreamLookupShardReadResult> ReadResults; std::unordered_set<TString> UniqueKeys; + TIntrusivePtr<TVectorIndexLevelsCache> VectorIndexLevelsCache; + std::deque<TCachedLevelTableDataPtr> PendingBatches; }; class TKqpJoinRows : public TKqpStreamLookupWorker { @@ -1231,7 +1321,8 @@ private: std::unique_ptr<TKqpStreamLookupWorker> CreateStreamLookupWorker(NKikimrKqp::TKqpStreamLookupSettings&& settings, ui64 taskId, const NMiniKQL::TTypeEnvironment& typeEnv, const NMiniKQL::THolderFactory& holderFactory, - const NYql::NDqProto::TTaskInput& inputDesc) { + const NYql::NDqProto::TTaskInput& inputDesc, + TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache) { TLookupSettings preparedSettings; preparedSettings.TablePath = std::move(settings.GetTable().GetPath()); @@ -1333,7 +1424,7 @@ std::unique_ptr<TKqpStreamLookupWorker> CreateStreamLookupWorker(NKikimrKqp::TKq case NKqpProto::EStreamLookupStrategy::LOOKUP: case NKqpProto::EStreamLookupStrategy::UNIQUE: case NKqpProto::EStreamLookupStrategy::LOCK_AND_LOOKUP: - return std::make_unique<TKqpLookupRows>(std::move(preparedSettings), typeEnv, holderFactory); + return std::make_unique<TKqpLookupRows>(std::move(preparedSettings), typeEnv, holderFactory, vectorIndexLevelsCache); case NKqpProto::EStreamLookupStrategy::JOIN: case NKqpProto::EStreamLookupStrategy::SEMI_JOIN: return std::make_unique<TKqpJoinRows>(std::move(preparedSettings), taskId, typeEnv, holderFactory, inputDesc); @@ -1349,8 +1440,8 @@ std::unique_ptr<TKqpStreamLookupWorker> CreateLookupWorker(TLookupSettings&& set AFL_ENSURE(!settings.KeepRowsOrder); AFL_ENSURE(!settings.AllowNullKeysPrefixSize); AFL_ENSURE(settings.InputColumns.size() <= settings.KeyColumns.size()); - - return std::make_unique<TKqpLookupRows>(std::move(settings), typeEnv, holderFactory); + + return std::make_unique<TKqpLookupRows>(std::move(settings), typeEnv, holderFactory, nullptr); } } // namespace NKqp diff --git a/ydb/core/kqp/runtime/kqp_stream_lookup_worker.h b/ydb/core/kqp/runtime/kqp_stream_lookup_worker.h index 4667ff0cdff..348093c84f5 100644 --- a/ydb/core/kqp/runtime/kqp_stream_lookup_worker.h +++ b/ydb/core/kqp/runtime/kqp_stream_lookup_worker.h @@ -1,5 +1,7 @@ #pragma once +#include "kqp_vector_index_levels_cache.h" + #include <ydb/core/protos/kqp.pb.h> #include <yql/essentials/minikql/mkql_node.h> #include <yql/essentials/minikql/computation/mkql_computation_node_holders.h> @@ -120,7 +122,7 @@ protected: std::unique_ptr<TKqpStreamLookupWorker> CreateStreamLookupWorker(NKikimrKqp::TKqpStreamLookupSettings&& settings, ui64 taskId, const NMiniKQL::TTypeEnvironment& typeEnv, const NMiniKQL::THolderFactory& holderFactory, - const NYql::NDqProto::TTaskInput& inputDesc); + const NYql::NDqProto::TTaskInput& inputDesc, TIntrusivePtr<TVectorIndexLevelsCache> vectorIndexLevelsCache); std::unique_ptr<TKqpStreamLookupWorker> CreateLookupWorker(TLookupSettings&& settings, const NMiniKQL::TTypeEnvironment& typeEnv, const NMiniKQL::THolderFactory& holderFactory); diff --git a/ydb/core/kqp/runtime/kqp_vector_index_levels_cache.cpp b/ydb/core/kqp/runtime/kqp_vector_index_levels_cache.cpp new file mode 100644 index 00000000000..6fd13f807a8 --- /dev/null +++ b/ydb/core/kqp/runtime/kqp_vector_index_levels_cache.cpp @@ -0,0 +1,158 @@ +#include "kqp_vector_index_levels_cache.h" +#include "kqp_read_iterator_common.h" + +#include <ydb/core/base/appdata.h> +#include <ydb/core/base/domain.h> +#include <ydb/core/base/tablet_pipecache.h> +#include <ydb/core/mon/mon.h> +#include <ydb/core/scheme/scheme_tablecell.h> +#include <ydb/core/tx/scheme_board/events.h> +#include <ydb/core/tx/scheme_board/subscriber.h> +#include <ydb/core/resource_pools/resource_pool_settings.h> +#include <ydb/core/cms/console/configs_dispatcher.h> +#include <ydb/core/cms/console/console.h> + +#include <ydb/library/actors/core/actor_bootstrapped.h> +#include <ydb/library/actors/core/hfunc.h> +#include <ydb/library/actors/core/log.h> +#include <ydb/library/services/services.pb.h> + +#include <library/cpp/monlib/service/pages/templates.h> + +#include <util/generic/algorithm.h> +#include <util/generic/hash.h> + +namespace NKikimr::NKqp { + +namespace { + +constexpr ui64 LevelCacheTxId = std::numeric_limits<ui64>::max() - 1; + +#define LOG_E(stream) LOG_ERROR_S(*TlsActivationContext, NKikimrServices::KQP_COMPUTE, "VectorIndexLevelsCacheMaintainer: " << stream) +#define LOG_N(stream) LOG_NOTICE_S(*TlsActivationContext, NKikimrServices::KQP_COMPUTE, "VectorIndexLevelsCacheMaintainer: " << stream) + + +class TVectorIndexLevelsCacheMaintainer + : public TActorBootstrapped<TVectorIndexLevelsCacheMaintainer> +{ + using TBase = TActorBootstrapped<TVectorIndexLevelsCacheMaintainer>; + +public: + explicit TVectorIndexLevelsCacheMaintainer( + TIntrusivePtr<TVectorIndexLevelsCache> cache, + std::shared_ptr<NRm::IKqpResourceManager> rm, + const NKikimrConfig::TTableServiceConfig::TResourceManager& initialConfig) + : Cache_(std::move(cache)) + , ResourceManager(rm) + , Tx(MakeIntrusive<NRm::TTxState>(ResourceManager, LevelCacheTxId, TInstant::Now(), NKikimr::NResourcePool::DEFAULT_POOL_ID, 100.0, AppData()->TenantName, false)) + , RmConfig(initialConfig) + {} + + struct TEvPrivate { + enum EEv { + EvIncreaseCacheSize = EventSpaceBegin(TEvents::ES_PRIVATE), + }; + + struct TEvIncreaseCacheSize: public TEventLocal<TEvIncreaseCacheSize, EEv::EvIncreaseCacheSize> {}; + }; + + void Bootstrap() { + Become(&TThis::StateWork); + UpdateCacheSize(); + + ui32 tableServiceConfigKind = (ui32) NKikimrConsole::TConfigItem::TableServiceConfigItem; + Send(NConsole::MakeConfigsDispatcherID(SelfId().NodeId()), + new NConsole::TEvConfigsDispatcher::TEvSetConfigSubscriptionRequest({tableServiceConfigKind}), + IEventHandle::FlagTrackDelivery); + } + + STATEFN(StateWork) { + switch (ev->GetTypeRewrite()) { + IgnoreFunc(NConsole::TEvConfigsDispatcher::TEvSetConfigSubscriptionResponse); + hFunc(NConsole::TEvConsole::TEvConfigNotificationRequest, Handle); + hFunc(TEvents::TEvUndelivered, Handle); + cFunc(TEvents::TSystem::Poison, PassAway); + cFunc(TEvPrivate::EvIncreaseCacheSize, UpdateCacheSize); + } + } + + void PassAway() override { + TBase::PassAway(); + } + + void Handle(TEvents::TEvUndelivered::TPtr& ev) { + switch (ev->Get()->SourceType) { + case NConsole::TEvConfigsDispatcher::EvSetConfigSubscriptionRequest: + LOG_E("Failed to deliver subscription request to config dispatcher"); + break; + case NConsole::TEvConsole::EvConfigNotificationResponse: + LOG_E("Failed to deliver config notification response"); + break; + default: + LOG_E("Undelivered event with unexpected source type: " << ev->Get()->SourceType); + break; + } + } + + void Handle(NConsole::TEvConsole::TEvConfigNotificationRequest::TPtr& ev) { + auto &event = ev->Get()->Record; + + auto newConfig = event.GetConfig().GetTableServiceConfig(); + + RmConfig.Swap(newConfig.MutableResourceManager()); + UpdateCacheSize(); + } + + void UpdateCacheSize() { + i64 maxCurrentSizeBytes = Cache_->MaxBytes(); + i64 leftBytes = static_cast<i64>(maxCurrentSizeBytes) - static_cast<i64>(Cache_->Bytes()); + ui64 increaseBatchSize = RmConfig.GetKqpLevelCacheIncreaseBatchSizeBytes(); + + ui64 maxAllowedSizeBytes = RmConfig.GetKqpLevelCacheMaxSizeBytes(); + + if ((maxCurrentSizeBytes == 0 || leftBytes < static_cast<i64>(increaseBatchSize)) + && static_cast<ui64>(maxCurrentSizeBytes) + increaseBatchSize <= maxAllowedSizeBytes) { + + auto res = ResourceManager->AllocateResources(*Tx, 1, NRm::TKqpResourcesRequest{.Memory=increaseBatchSize}); + if (res) { + Cache_->SetMaxBytes(maxCurrentSizeBytes + increaseBatchSize); + LOG_N("Altered max bytes to " << HumanReadableSize(maxCurrentSizeBytes + increaseBatchSize, ESizeFormat::SF_BYTES) + << ", prev size " << HumanReadableSize(maxCurrentSizeBytes, ESizeFormat::SF_BYTES)); + } + + } else if (maxAllowedSizeBytes < static_cast<ui64>(maxCurrentSizeBytes)) { + ui64 deficit = static_cast<ui64>(maxCurrentSizeBytes) - maxAllowedSizeBytes; + ui64 change = std::min(increaseBatchSize, deficit); + ResourceManager->FreeResources(*Tx, 1, NRm::TKqpResourcesRequest{.Memory=change}); + i64 newSize = maxCurrentSizeBytes - static_cast<i64>(change); + Cache_->SetMaxBytes(newSize); + LOG_N("Altered max bytes to " << HumanReadableSize(newSize, ESizeFormat::SF_BYTES) + << ", prev size " << HumanReadableSize(maxCurrentSizeBytes, ESizeFormat::SF_BYTES)); + } + + Schedule(TDuration::Seconds(1), new TEvPrivate::TEvIncreaseCacheSize); + } + +private: + TIntrusivePtr<TVectorIndexLevelsCache> Cache_; + + std::shared_ptr<NRm::IKqpResourceManager> ResourceManager; + TIntrusivePtr<NRm::TTxState> Tx; + NKikimrConfig::TTableServiceConfig::TResourceManager RmConfig; +}; + +#undef LOG_E +#undef LOG_N + +} // anonymous namespace + +IActor* CreateVectorIndexLevelsCacheMaintainer( + TIntrusivePtr<TVectorIndexLevelsCache> cache, + std::shared_ptr<NRm::IKqpResourceManager> rm, + const NKikimrConfig::TTableServiceConfig::TResourceManager& initialConfig) +{ + return new TVectorIndexLevelsCacheMaintainer(std::move(cache), std::move(rm), initialConfig); +} + + +} // namespace NKikimr::NKqp diff --git a/ydb/core/kqp/runtime/kqp_vector_index_levels_cache.h b/ydb/core/kqp/runtime/kqp_vector_index_levels_cache.h new file mode 100644 index 00000000000..3761d15f46d --- /dev/null +++ b/ydb/core/kqp/runtime/kqp_vector_index_levels_cache.h @@ -0,0 +1,216 @@ +#pragma once + +#include <ydb/core/base/events.h> +#include <ydb/core/scheme/scheme_pathid.h> +#include <ydb/core/tx/datashard/datashard.h> + +#include <ydb/library/actors/core/actorid.h> +#include <ydb/library/actors/core/event_local.h> +#include <ydb/library/actors/core/events.h> + +#include <ydb/core/scheme/scheme_tablecell.h> +#include <ydb/core/kqp/rm_service/kqp_rm_service.h> +#include <library/cpp/cache/cache.h> +#include <library/cpp/monlib/dynamic_counters/counters.h> + +#include <util/datetime/base.h> +#include <util/generic/hash.h> +#include <util/generic/ptr.h> +#include <util/generic/string.h> +#include <util/generic/vector.h> +#include <util/system/rwlock.h> + +#include <atomic> + +namespace NKikimr::NKqp { + +// Cached level table data - stores rows in a format ready for zero-copy access +// Uses shared_ptr for safe concurrent access without copying +struct TCachedLevelTableData : public TAtomicRefCount<TCachedLevelTableData> { + // Store serialized cell vectors directly - no deserialization needed + TOwnedCellVecBatch BatchRows; + + TCachedLevelTableData() = default; +}; + +using TCachedLevelTableDataPtr = TIntrusivePtr<TCachedLevelTableData>; + +struct TLevelTableCacheKey { + TPathId TableId; + TString SerializedKeys; + + bool operator==(const TLevelTableCacheKey& other) const { + return TableId == other.TableId && SerializedKeys == other.SerializedKeys; + } + + size_t Hash() const { + return CombineHashes(THash<TPathId>()(TableId), THash<TString>()(SerializedKeys)); + } +}; + +} // namespace NKikimr::NKqp + +template <> +struct THash<NKikimr::NKqp::TLevelTableCacheKey> { + size_t operator()(const NKikimr::NKqp::TLevelTableCacheKey& key) const { + return key.Hash(); + } +}; + +namespace NKikimr::NKqp { + +// LRU cache entry wrapper +struct TLevelTableCacheEntry { + TCachedLevelTableDataPtr Data; + size_t ByteSize = 0; // approximate memory footprint, computed once at insertion + + TLevelTableCacheEntry() = default; + TLevelTableCacheEntry(TCachedLevelTableDataPtr data, size_t byteSize) + : Data(std::move(data)) + , ByteSize(byteSize) + {} +}; + +// Approximate memory footprint of cached data. Used both to drive the bytes +// gauge and to give operators a sense of cache pressure. +inline size_t EstimateCachedDataBytes(const TCachedLevelTableData& data) { + return data.BatchRows.DataSizeEstimate(); +} + +class TVectorIndexLevelsCache : public TThrRefBase { +public: + using TPtr = TIntrusivePtr<TVectorIndexLevelsCache>; + +public: + + // Try to get cached data - returns shared pointer (zero-copy) + TCachedLevelTableDataPtr Get(const TPathId& tableId, const TString& serializedKeys) { + TWriteGuard guard(Lock_); + + TLevelTableCacheKey key{tableId, serializedKeys}; + auto it = Cache_.Find(key); + if (it != Cache_.End()) { + Hits_->Inc(); + return it.Value().Data; + } + Misses_->Inc(); + return nullptr; + } + + // Store data in cache + void Put(const TPathId& tableId, const TString& serializedKeys, TCachedLevelTableDataPtr data) { + const size_t bytes = data ? EstimateCachedDataBytes(*data) : 0; + + TWriteGuard guard(Lock_); + + TLevelTableCacheKey key{tableId, serializedKeys}; + + if (auto existing = Cache_.FindWithoutPromote(key); existing != Cache_.End()) { + return; + } else { + Evict(bytes); + Cache_.Insert(key, TLevelTableCacheEntry(std::move(data), bytes)); + BytesTotal_ += bytes; + Inserts_->Inc(); + RefreshGauges(); + } + } + + void Evict(ui64 bytesToAdd = 0) { + // Byte-cap eviction. The TLRUCache only caps by entry count; with + // multi-KB rows (embedding columns) one entry can be megabytes, so + // the entry-count cap alone can let total bytes balloon. + auto maxBytes = MaxCacheBytes_.load(); + while (BytesTotal_ + bytesToAdd > maxBytes && Cache_.Size() >= 1) { + auto oldest = Cache_.FindOldest(); + Y_ENSURE(oldest != Cache_.End()); + BytesTotal_ -= oldest.Value().ByteSize; + Cache_.Erase(oldest); + Invalidations_->Inc(); + } + } + + // Number of cached entries. Cheap; primarily useful for tests. + size_t Size() const { + TReadGuard guard(Lock_); + return Cache_.Size(); + } + + size_t MaxBytes() const { return MaxCacheBytes_.load(); } + + void SetMaxBytes(ui64 maxBytes) { + MaxCacheBytes_.store(maxBytes); + + TWriteGuard guard(Lock_); + + Evict(); + + RefreshGauges(); + } + + // Counter getters for monitoring (cumulative since process start). + ui64 HitsTotal() const { return Hits_ ? Hits_->Val() : 0; } + ui64 MissesTotal() const { return Misses_ ? Misses_->Val() : 0; } + ui64 InsertsTotal() const { return Inserts_ ? Inserts_->Val() : 0; } + ui64 InvalidationsTotal() const { return Invalidations_ ? Invalidations_->Val() : 0; } + + // Approximate bytes currently held in the cache. O(1). + size_t Bytes() const { + TReadGuard guard(Lock_); + return BytesTotal_; + } + + // Counters argument: a parent group under which the cache exposes its + // metrics. Pass nullptr in tests / contexts without a counters tree — the + // cache attaches to a private group and metric updates become no-op-ish. + // maxCacheBytes caps total cached bytes (rows + metadata) + explicit TVectorIndexLevelsCache(::NMonitoring::TDynamicCounterPtr counters = nullptr) + : MaxCacheBytes_(0) + , Cache_(MaxCacheEntries_) + { + auto group = counters + ? counters->GetSubgroup("subsystem", "VectorIndexLevelsCache") + : MakeIntrusive<::NMonitoring::TDynamicCounters>(); + + MaxSizeGauge_ = group->GetCounter("MaxSize", false); + BytesGauge_ = group->GetCounter("Bytes", false); + EntriesGauge_ = group->GetCounter("Entries", false); + Hits_ = group->GetCounter("Hits", true); + Misses_ = group->GetCounter("Misses", true); + Inserts_ = group->GetCounter("Inserts", true); + Invalidations_ = group->GetCounter("Invalidations", true); + } + +private: + // Level tables are immutable, so cache can be long-lived. Entry-count + // cap is a coarse upper bound; the byte cap below is the active limit. + static constexpr size_t MaxCacheEntries_ = 10000; + + // Must be called with the write lock held. + void RefreshGauges() { + BytesGauge_->Set(BytesTotal_); + EntriesGauge_->Set(Cache_.Size()); + MaxSizeGauge_->Set(MaxCacheBytes_.load()); + } + + std::atomic<ui64> MaxCacheBytes_; + size_t BytesTotal_ = 0; + + mutable TRWMutex Lock_; + TLRUCache<TLevelTableCacheKey, TLevelTableCacheEntry> Cache_; + + ::NMonitoring::TDynamicCounters::TCounterPtr MaxSizeGauge_; + ::NMonitoring::TDynamicCounters::TCounterPtr BytesGauge_; + ::NMonitoring::TDynamicCounters::TCounterPtr EntriesGauge_; + ::NMonitoring::TDynamicCounters::TCounterPtr Hits_; + ::NMonitoring::TDynamicCounters::TCounterPtr Misses_; + ::NMonitoring::TDynamicCounters::TCounterPtr Inserts_; + ::NMonitoring::TDynamicCounters::TCounterPtr Invalidations_; +}; + +IActor* CreateVectorIndexLevelsCacheMaintainer( + TIntrusivePtr<TVectorIndexLevelsCache> cache, + std::shared_ptr<NRm::IKqpResourceManager> rm, + const NKikimrConfig::TTableServiceConfig::TResourceManager& initialConfig); + +} // namespace NKikimr::NKqp
\ No newline at end of file diff --git a/ydb/core/kqp/runtime/ya.make b/ydb/core/kqp/runtime/ya.make index 2871362863a..c98dd18cb71 100644 --- a/ydb/core/kqp/runtime/ya.make +++ b/ydb/core/kqp/runtime/ya.make @@ -27,6 +27,8 @@ SRCS( kqp_stream_lock_worker.cpp kqp_stream_lock_worker.h kqp_transport.cpp + kqp_vector_index_levels_cache.cpp + kqp_vector_index_levels_cache.h kqp_vector_actor.cpp kqp_write_actor_settings.cpp kqp_write_actor.cpp @@ -51,9 +53,11 @@ PEERDIR( ydb/core/formats ydb/core/kqp/common ydb/core/kqp/common/buffer + ydb/core/mon ydb/core/persqueue/events ydb/core/protos ydb/core/scheme + ydb/core/tx/scheme_board ydb/core/ydb_convert ydb/library/aclib ydb/library/yql/dq/actors diff --git a/ydb/core/kqp/ut/indexes/vector/kqp_indexes_vector_ut.cpp b/ydb/core/kqp/ut/indexes/vector/kqp_indexes_vector_ut.cpp index 6e6582a5596..5bc89a334aa 100644 --- a/ydb/core/kqp/ut/indexes/vector/kqp_indexes_vector_ut.cpp +++ b/ydb/core/kqp/ut/indexes/vector/kqp_indexes_vector_ut.cpp @@ -1361,7 +1361,10 @@ Y_UNIT_TEST_SUITE(KqpVectorIndexes) { Y_UNIT_TEST_QUAD(VectorSearchPushdown, Covered, Followers) { auto setting = NKikimrKqp::TKqpSetting(); - auto serverSettings = TKikimrSettings() + NKikimrConfig::TAppConfig appConfig; + appConfig.MutableTableServiceConfig()->MutableResourceManager()->SetKqpLevelCacheMaxSizeBytes(300_MB); + + auto serverSettings = TKikimrSettings(appConfig) // SetUseRealThreads(false) is required to capture events (!) but then you have to do kikimr.RunCall() for everything .SetUseRealThreads(false) .SetEnableForceFollowers(Followers) @@ -1418,7 +1421,10 @@ Y_UNIT_TEST_SUITE(KqpVectorIndexes) { // Check that level & posting are read from followers UNIT_ASSERT(isFollower == (Followers && (shardType == levelType || shardType == postingType))); auto & read = ev->Get<TEvDataShard::TEvRead>()->Record; - if (shardType == (Covered ? mainType : postingType)) { + if (shardType == levelType) { + // Level table reads do full scan for caching (VectorTopK cleared by cache layer) + UNIT_ASSERT(!read.HasVectorTopK()); + } else if (shardType == (Covered ? mainType : postingType)) { // Non-covering index does topK on main table, covering does it on posting UNIT_ASSERT(!read.HasVectorTopK()); } else { @@ -1427,10 +1433,7 @@ Y_UNIT_TEST_SUITE(KqpVectorIndexes) { auto & topK = read.GetVectorTopK(); // Check that target and limit are pushed down UNIT_ASSERT(topK.GetTargetVector() == "\x67\x71\x02"); - if (shardType == levelType) { - // Equal to pragma - UNIT_ASSERT(topK.GetLimit() == 2); - } else if (shardType == (Covered ? postingType : mainType)) { + if (shardType == (Covered ? postingType : mainType)) { // Equal to LIMIT UNIT_ASSERT(topK.GetLimit() == 3); } @@ -1549,6 +1552,229 @@ Y_UNIT_TEST_SUITE(KqpVectorIndexes) { WITH (parallel=2); )", EnableIndexStreamWrite); } + + Y_UNIT_TEST(VectorIndexLevelTableCache) { + NKikimrConfig::TAppConfig appConfig; + appConfig.MutableTableServiceConfig()->MutableResourceManager()->SetKqpLevelCacheMaxSizeBytes(300_MB); + + auto setting = NKikimrKqp::TKqpSetting(); + auto serverSettings = TKikimrSettings(appConfig) + // SetUseRealThreads(false) is required to capture events + .SetUseRealThreads(false) + .SetKqpSettings({setting}); + + TKikimrRunner kikimr(serverSettings); + auto runtime = kikimr.GetTestServer().GetRuntime(); + + const int flags = F_NULLABLE; + auto db = kikimr.RunCall([&] { return kikimr.GetTableClient(); }); + auto session = kikimr.RunCall([&] { return DoCreateTableAndVectorIndex(db, flags); }); + + // Resolve level table shards to track reads + constexpr ui32 levelType = 1, postingType = 2, mainType = 3; + THashMap<TActorId, ui32> actorTypes; + auto resolveActors = [&](const char* tableName, ui32 type) { + auto shards = GetTableShards(&kikimr.GetTestServer(), runtime->AllocateEdgeActor(), tableName); + for (auto shardId: shards) { + auto actorId = ResolveTablet(*runtime, shardId); + actorTypes[actorId] = type; + } + }; + resolveActors("/Root/TestTable/index1/indexImplLevelTable", levelType); + resolveActors("/Root/TestTable/index1/indexImplPostingTable", postingType); + resolveActors("/Root/TestTable", mainType); + + // Counter for level table reads + std::atomic<int> levelTableReadCount{0}; + std::atomic<int> postingTableReadCount{0}; + + auto captureEvents = [&](TTestActorRuntimeBase&, TAutoPtr<IEventHandle>& ev) { + if (ev->GetTypeRewrite() == TEvDataShard::TEvRead::EventType) { + ui32 shardType = actorTypes[ev->GetRecipientRewrite()]; + if (shardType == levelType) { + levelTableReadCount++; + } else if (shardType == postingType) { + postingTableReadCount++; + } + } + return false; + }; + runtime->SetEventFilter(captureEvents); + + const TString query1(Q_(R"( + pragma ydb.KMeansTreeSearchTopSize = "2"; + $TargetEmbedding = String::HexDecode("677102"); + SELECT * FROM `/Root/TestTable` + VIEW index1 ORDER BY Knn::CosineDistance(emb, $TargetEmbedding) + LIMIT 3 + )")); + + // First query - should read from level table + { + levelTableReadCount = 0; + postingTableReadCount = 0; + + auto result = kikimr.RunCall([&] { + return session.ExecuteDataQuery(query1, TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()) + .ExtractValueSync(); + }); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); + + // First query should read from level table + UNIT_ASSERT_C(levelTableReadCount.load() > 0, + "First query should read from level table, but read count is " << levelTableReadCount.load()); + Cerr << "First query: level table reads = " << levelTableReadCount.load() + << ", posting table reads = " << postingTableReadCount.load() << Endl; + } + + int firstLevelReads = levelTableReadCount.load(); + + // Second query with the same target - should NOT read from level table (cached) + { + levelTableReadCount = 0; + postingTableReadCount = 0; + + auto result = kikimr.RunCall([&] { + return session.ExecuteDataQuery(query1, TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()) + .ExtractValueSync(); + }); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); + + Cerr << "Second query: level table reads = " << levelTableReadCount.load() + << ", posting table reads = " << postingTableReadCount.load() << Endl; + + // Second query should NOT read from level table (cached) + UNIT_ASSERT_C(levelTableReadCount.load() == 0, + "Second query should use cached level table data, but level table read count is " + << levelTableReadCount.load() << " (first query had " << firstLevelReads << ")"); + } + + // Third query - should still use cache + { + levelTableReadCount = 0; + postingTableReadCount = 0; + + auto result = kikimr.RunCall([&] { + return session.ExecuteDataQuery(query1, TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()) + .ExtractValueSync(); + }); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); + + Cerr << "Third query: level table reads = " << levelTableReadCount.load() + << ", posting table reads = " << postingTableReadCount.load() << Endl; + + // Third query should NOT read from level table (still cached) + UNIT_ASSERT_C(levelTableReadCount.load() == 0, + "Third query should use cached level table data, but level table read count is " + << levelTableReadCount.load()); + } + } + + Y_UNIT_TEST(VectorIndexLevelTableCacheAcrossSnapshots) { + // Test that level table cache works across different snapshots + // Level tables are immutable after index creation, so cache should hit + // regardless of snapshot changes + + NKikimrConfig::TAppConfig appConfig; + appConfig.MutableTableServiceConfig()->MutableResourceManager()->SetKqpLevelCacheMaxSizeBytes(300_MB); + auto setting = NKikimrKqp::TKqpSetting(); + auto serverSettings = TKikimrSettings(appConfig) + .SetUseRealThreads(false) + .SetKqpSettings({setting}); + + TKikimrRunner kikimr(serverSettings); + auto runtime = kikimr.GetTestServer().GetRuntime(); + + const int flags = F_NULLABLE; + auto db = kikimr.RunCall([&] { return kikimr.GetTableClient(); }); + auto session = kikimr.RunCall([&] { return DoCreateTableAndVectorIndex(db, flags); }); + + // Resolve level table shards to track reads + constexpr ui32 levelType = 1; + THashMap<TActorId, ui32> actorTypes; + auto shards = GetTableShards(&kikimr.GetTestServer(), runtime->AllocateEdgeActor(), + "/Root/TestTable/index1/indexImplLevelTable"); + for (auto shardId: shards) { + auto actorId = ResolveTablet(*runtime, shardId); + actorTypes[actorId] = levelType; + } + + std::atomic<int> levelTableReadCount{0}; + auto captureEvents = [&](TTestActorRuntimeBase&, TAutoPtr<IEventHandle>& ev) { + if (ev->GetTypeRewrite() == TEvDataShard::TEvRead::EventType) { + if (actorTypes[ev->GetRecipientRewrite()] == levelType) { + levelTableReadCount++; + } + } + return false; + }; + runtime->SetEventFilter(captureEvents); + + const TString query(Q_(R"( + pragma ydb.KMeansTreeSearchTopSize = "2"; + $TargetEmbedding = String::HexDecode("677102"); + SELECT * FROM `/Root/TestTable` + VIEW index1 ORDER BY Knn::CosineDistance(emb, $TargetEmbedding) + LIMIT 3 + )")); + + // First query - should read from level table (cache miss) + { + levelTableReadCount = 0; + auto result = kikimr.RunCall([&] { + return session.ExecuteDataQuery(query, TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()) + .ExtractValueSync(); + }); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); + UNIT_ASSERT_C(levelTableReadCount.load() > 0, + "First query should read from level table, got " << levelTableReadCount.load()); + Cerr << "Query 1 (first session tx): level reads = " << levelTableReadCount.load() << Endl; + } + + // Second query in NEW TRANSACTION (different snapshot!) - should still hit cache + // because level tables are immutable + { + levelTableReadCount = 0; + auto result = kikimr.RunCall([&] { + return session.ExecuteDataQuery(query, TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()) + .ExtractValueSync(); + }); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); + Cerr << "Query 2 (new tx, different snapshot): level reads = " << levelTableReadCount.load() << Endl; + UNIT_ASSERT_C(levelTableReadCount.load() == 0, + "Second query should use cache despite different snapshot, got " << levelTableReadCount.load()); + } + + // Third query in yet another transaction - should still use cache + { + levelTableReadCount = 0; + auto result = kikimr.RunCall([&] { + return session.ExecuteDataQuery(query, TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()) + .ExtractValueSync(); + }); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); + Cerr << "Query 3 (another new tx): level reads = " << levelTableReadCount.load() << Endl; + UNIT_ASSERT_C(levelTableReadCount.load() == 0, + "Third query should use cache, got " << levelTableReadCount.load()); + } + + // Create a NEW session (completely different connection) - should still hit cache + auto session2 = kikimr.RunCall([&] { + return db.CreateSession().GetValueSync().GetSession(); + }); + { + levelTableReadCount = 0; + auto result = kikimr.RunCall([&] { + return session2.ExecuteDataQuery(query, TTxControl::BeginTx(TTxSettings::SerializableRW()).CommitTx()) + .ExtractValueSync(); + }); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); + Cerr << "Query 4 (new session): level reads = " << levelTableReadCount.load() << Endl; + UNIT_ASSERT_C(levelTableReadCount.load() == 0, + "Query from new session should use cache, got " << levelTableReadCount.load()); + } + } + } } diff --git a/ydb/core/protos/table_service_config.proto b/ydb/core/protos/table_service_config.proto index a13664141e8..14312e4dbc7 100644 --- a/ydb/core/protos/table_service_config.proto +++ b/ydb/core/protos/table_service_config.proto @@ -59,6 +59,9 @@ message TTableServiceConfig { optional uint64 MaxNonParallelDataQueryTasksLimit = 28 [default = 1000]; optional bool VerboseMemoryLimitException = 29 [default = false]; + + optional uint64 KqpLevelCacheMaxSizeBytes = 31 [default = 0]; + optional uint64 KqpLevelCacheIncreaseBatchSizeBytes = 32 [default = 33554432]; } message TSpillingServiceConfig { |
