Imported from kuzudb/kuzu#6047
Original URL: kuzudb#6047
Original author: @1amageek
Original created at: 2025-10-05T01:41:23Z
Problem
Database initialization is blocked by sequential HNSW vector index loading, causing significant delays that scale linearly with the number of indexed records.
Measured Impact:
| Records |
Init Time |
User Experience |
| 1,000 |
~2s |
Noticeable |
| 10,000 |
~10s |
Unacceptable |
| 100,000 |
~60s+ |
Critical |
Real-world impact: iOS app with 10,000+ photo records experiences 10+ second white screen on startup.
Root Cause
HNSW indexes are loaded sequentially within a recovery transaction during database initialization:
// extension_manager.cpp:93-97
void ExtensionManager::autoLoadLinkedExtensions(main::ClientContext* context) {
auto trxContext = transaction::TransactionContext::Get(*context);
trxContext->beginRecoveryTransaction(); // ⬅️ Single-threaded lock
loadLinkedExtensions(context, loadedExtensions); // ⬅️ Sequential
trxContext->commit();
}
// vector_extension.cpp:10-29
static void initHNSWEntries(main::ClientContext* context) {
for (auto& indexEntry : catalog->getIndexEntries()) {
if (indexEntry->getIndexType() == HNSWIndexCatalogEntry::TYPE_NAME) {
unloadedIndex.load(context, storageManager); // ⬅️ O(N) per index
}
}
}
Bottleneck: Each HNSW index loads graph structure (O(N) complexity) one at a time.
Log evidence:
[KUZU DEBUG] Checkpointer: auto-loading linked extensions
[KUZU DEBUG] IndexHolder::load() START - indexType='HNSW'
[KUZU DEBUG] IndexHolder: calling loadFunc()
⬆️ This takes 10+ seconds for 10k records
[KUZU DEBUG] IndexHolder::load() COMPLETE
Proposed Solution
Key observation: After WAL replay completes, the database is in a consistent read-only state where:
- ✅ All transactions applied (data consistent)
- ✅ No concurrent writes (initialization phase)
- ✅ HNSW indexes are independent (no shared state)
- ✅ Checkpoint data is immutable (thread-safe reads)
Therefore: Transaction locking is unnecessary, and indexes can be loaded in parallel.
Implementation Suggestion
// extension_manager.cpp - Add parallel loading path
void ExtensionManager::autoLoadLinkedExtensions(main::ClientContext* context) {
// Check if parallel loading is safe
if (isConsistentAfterWAL() && canLoadInParallel()) {
loadIndexesInParallel(context); // NEW: Parallel path
return;
}
// Fallback: Sequential loading (existing behavior)
auto trxContext = transaction::TransactionContext::Get(*context);
trxContext->beginRecoveryTransaction();
loadLinkedExtensions(context, loadedExtensions);
trxContext->commit();
}
void ExtensionManager::loadIndexesInParallel(main::ClientContext* context) {
std::vector<std::thread> workers;
for (auto& indexEntry : getHNSWIndexes()) {
workers.emplace_back([&]() {
// Load without transaction lock (read-only, post-WAL)
indexEntry.loadWithoutTransaction(context, storageManager);
});
}
for (auto& worker : workers) {
worker.join();
}
}
Safety Analysis
Why parallel loading is safe:
- Post-WAL consistency: Data is stable after
WALReplayer::replay()
- Read-only operations: HNSW loading only reads checkpoint data
- Index independence: Each index operates on different table/column
- Thread-safety: All operations are on thread-local objects
Expected Impact
Performance improvement (based on Amdahl's Law with 95% parallelizable work):
| CPU Cores |
Current |
Parallel |
Speedup |
| 2 |
10.0s |
5.5s |
1.8x |
| 4 |
10.0s |
3.0s |
3.3x |
| 8 |
10.0s |
1.8s |
5.6x |
User experience:
- Mobile apps: Sub-second startup instead of 10+ seconds
- Desktop apps: Near-instant database initialization
- Large datasets: Scales better with multi-core systems
Implementation Considerations
- Backward compatibility: Add as opt-in feature with fallback to sequential loading
- Configuration:
SystemConfig::enableParallelIndexLoading (default: true)
- Thread limit:
SystemConfig::maxIndexLoadThreads (default: auto-detect cores)
- Error handling: Graceful fallback to sequential on any thread error
Additional Context
- Platform: Tested on iOS (ARM64) with 10,000+ photo records
- Use case: Photo mosaic app with LAB color vector similarity search
- Current workaround: Defer GraphContainer initialization to background thread (UI still blocked)
Would appreciate feedback on this approach. Happy to provide more details or help with implementation if this aligns with Kuzu's roadmap.
Imported from
kuzudb/kuzu#6047Original URL: kuzudb#6047
Original author: @1amageek
Original created at: 2025-10-05T01:41:23Z
Problem
Database initialization is blocked by sequential HNSW vector index loading, causing significant delays that scale linearly with the number of indexed records.
Measured Impact:
Real-world impact: iOS app with 10,000+ photo records experiences 10+ second white screen on startup.
Root Cause
HNSW indexes are loaded sequentially within a recovery transaction during database initialization:
Bottleneck: Each HNSW index loads graph structure (O(N) complexity) one at a time.
Log evidence:
Proposed Solution
Key observation: After WAL replay completes, the database is in a consistent read-only state where:
Therefore: Transaction locking is unnecessary, and indexes can be loaded in parallel.
Implementation Suggestion
Safety Analysis
Why parallel loading is safe:
WALReplayer::replay()Expected Impact
Performance improvement (based on Amdahl's Law with 95% parallelizable work):
User experience:
Implementation Considerations
SystemConfig::enableParallelIndexLoading(default: true)SystemConfig::maxIndexLoadThreads(default: auto-detect cores)Additional Context
Would appreciate feedback on this approach. Happy to provide more details or help with implementation if this aligns with Kuzu's roadmap.