perf(sindi): batch posting normalization for incremental add - #2693
perf(sindi): batch posting normalization for incremental add#2693CharlesXu-HQ wants to merge 1 commit into
Conversation
|
/label status/waiting-for-review |
Merge Protections🟢 All 2 merge protections satisfied — ready to merge. Show 2 satisfied protections🟢 Require kind label
🟢 Require version label
|
LHT129
left a comment
There was a problem hiding this comment.
Overall this is a well-engineered PR with thorough test coverage and detailed performance benchmarks. The core idea — deferring full posting-list normalization by maintaining a sorted prefix plus a bounded sorted dirty suffix — is sound and delivers significant incremental Add throughput improvements (up to 10x for batch-size-1).
Summary of findings:
-
[suggestion]
const_castin Serialize methods — Theconst_cast<SINDI*>(this)->cal_memory_usage()calls break the logical const contract. Consider making the cached memory usagemutableor refactoring the normalization+recalc into a pre-serialization helper. -
[note] Serialize lock upgrade — Changing from
shared_locktoscoped_lockin serialization is a necessary trade-off to normalize dirty postings before writing. The PR description already documents this; no action needed. -
[suggestion] Redundant
ScanPostingRangecall on hot path — When postings are fully normalized (the common case), the secondScanPostingRangecall hasterm_count=0and returns immediately. A guard onsuffix_count > 0would avoid the function call overhead.
What is done well:
- The binary-search partition in
SelectPostingRunsis correct and well-tested across all three quantization types. ForEachSelectedPostingelegantly merges two sorted runs in heap-insertion order without materializing a merged array.- Test coverage is comprehensive: deterministic correctness, serialization round-trips, filter paths, randomized stress tests (20 seeds), and all three quantization encodings.
- The normalization threshold clamping (
MIN_DIRTY_POSTING_SIZE=32,MAX_DIRTY_POSTING_SIZE=256) provides a good balance between deferring work and bounding the dirty suffix size. - Performance data is thorough and reproducible, with identical A/B result hashes confirming correctness.
LHT129
left a comment
There was a problem hiding this comment.
Overall this is a well-engineered optimization with thorough test coverage. The performance improvements are impressive (up to 10x for single-vector incremental Add).
Suggestions:
-
src/algorithm/sindi/sindi.cpp:1587and:1737—[suggestion]TheSerializeandserialize_streaming_bodymethods are markedconstbut modify internal state by callingNormalizeDirtyPostings()(which mutates posting lists) andcal_memory_usage()viaconst_cast. This violates logical const-correctness — callers of aconstmethod do not expect side effects that change the object's observable state. Consider making these methods non-const, or marking the affected mutable state asmutablewith documentation that serialization is a normalization boundary. -
src/datacell/sparse_term_datacell.cpp:143—[note]The binary search inSelectPostingRunscorrectly partitions two sorted runs. The fallthroughCHECK_ARGUMENT(false, ...)is an unreachable defensive guard — consistent with the codebase convention. The four uncovered lines noted in the PR description correspond to this and similar defensive paths.
|
/kind improvement |
26ebccd to
25e45f8
Compare
LHT129
left a comment
There was a problem hiding this comment.
This is a well-executed optimization that defers full posting-list re-sorting during incremental Add by maintaining a sorted prefix plus a bounded sorted dirty suffix. The core algorithm (SelectPostingRuns binary search, ForEachSelectedPosting merge, NormalizePosting two-way merge) is correct and well-tested across all three quantization types with deterministic, randomized, and lifecycle coverage.
Summary of observations (no blocking issues):
-
The
const_castremoval (viamutableoncurrent_memory_usage_andconstoncal_memory_usage()) cleanly resolves the const-correctness concern in serialization paths. -
The exclusive lock in
Serialize/serialize_streaming_bodyis a necessary trade-off to guarantee postings are normalized before writing, and is clearly documented in the PR description. -
The
query_implsuffix scan guard (if (selected.suffix_count > 0)) noted in a prior review comment has been addressed — this avoids an unnecessary function call on the hot path for fully normalized postings. -
The
Compact()method callsFinalizeInsertBatch()defensively but does not callNormalizeDirtyPostings(). This is safe because all current callers normalize before compacting, but future callers should be aware of this invariant. -
Test coverage is thorough: 98.28% changed-line coverage, all three quantization types, KNN/range/filter paths, serialization round-trips, randomized differential testing with 20 seeds, and wire format version retention.
The performance improvements (up to 10x for single-vector incremental adds) are well-supported by controlled A/B benchmarks with identical KNN/range/serialized hashes confirming result correctness.
LHT129
left a comment
There was a problem hiding this comment.
I have reviewed this PR and the changes look solid. The deferred normalization strategy for incremental posting batches is well-designed and the test coverage is comprehensive.
Summary of changes:
SparseTermDataCell: Introduces two-run posting lists with deferred normalization viaFinalizeInsertBatch()andNormalizeDirtyPostings(). Query and heap-insert paths correctly handle split runs throughSelectPostingRuns()andForEachSelectedPosting().SINDI::Add(): Replaces eagerSortByValue()with batchedFinalizeInsertBatch()and defers full normalization until a window reaches capacity.SINDI::Build(): Normalizes dirty postings before compacting.- Serialization (
Serialize/serialize_streaming_body): Upgraded to exclusive lock and normalizes dirty postings before writing to ensure canonical output. cal_memory_usage(): Madeconstwithmutablememory-usage atomic.- Tests: Updated existing test, added serialization normalization test, and added comprehensive
SelectPostingRunsand two-run heap equivalence tests.
Key observations:
-
The binary search in
SelectPostingRunscorrectly finds the optimal partition between sorted prefix and suffix runs. The monotonicity property holds because as more entries are taken from the suffix, the boundary entries move in consistent directions. -
The
ForEachSelectedPostingtemplate correctly merges two sorted runs in value-descending order, maintaining the same traversal semantics as the original single-run iteration. -
The lock upgrade in serialization from shared to exclusive is necessary since normalization mutates internal state. This is an intentional trade-off: serialization now blocks concurrent reads but produces canonical output.
-
The
term >= term_ids_.size() || term_sizes_[term] == 0guard added toInsertHeapByTermListsfixes a missing null-pointer check that was already present inquery_impl.
No blocking issues found. The existing review comments have been properly addressed (mutable atomic, exclusive lock rationale, suffix_count zero-guard).
LHT129
left a comment
There was a problem hiding this comment.
I have completed a thorough review of this PR. The implementation is well-structured and the test coverage is comprehensive (976 additions across 7 files with extensive unit tests covering all three quantization types, serialization round-trips, randomized differential testing, and heap equivalence).
Summary of findings:
The core approach — maintaining each mutable SINDI posting list as a sorted prefix plus a bounded sorted dirty suffix — is sound. The binary search in SelectPostingRuns correctly partitions the two sorted runs, and the merge logic in both ForEachSelectedPosting (for heap insertion) and NormalizePosting (for full normalization) is correct.
Previous review comments addressed:
- The
const_castin serialization has been eliminated by makingcurrent_memory_usage_mutable andcal_memory_usage()const. - The unconditional second
ScanPostingRangecall inquery_implis now guarded byif (selected.suffix_count > 0). - The exclusive lock for serialization is confirmed as intentional and documented.
Remaining notes (non-blocking, already raised by LHT129):
normalize_dirty_postings_for_serialization()is declaredconstbut mutates posting list internals — the existing comment explains the logical const semantics.- Scratch vector allocations in
FinalizeInsertBatchare created on the stack before the early-return check — negligible overhead.
The performance data in the PR description shows impressive improvements (up to 10x for single-vector adds) with no regression in search accuracy or serialization compatibility. The controlled A/B methodology is rigorous.
No blocking issues found. The PR looks ready to merge from a code review perspective.
LHT129
left a comment
There was a problem hiding this comment.
I have completed a thorough review of this PR. The existing inline comments from the previous review round have already covered the main areas of concern, and the author has addressed several of them (e.g., making current_memory_usage_ mutable, guarding the suffix scan).
Summary of my review:
Correctness: The core algorithm — SelectPostingRuns binary search, NormalizePosting two-way merge, and ForEachSelectedPosting merge-traversal — all appear correct. The PostingEntryBefore comparator correctly implements descending-by-value with ascending-by-id tiebreaking, consistent with the existing SortPostingListByValue ordering.
Concurrency: The lock upgrade from shared_lock to scoped_lock in Serialize/serialize_streaming_body is intentional and necessary for the normalization-before-write guarantee. Search paths continue to use shared locking, and the dirty-posting data structures are only mutated under the exclusive lock.
Memory: The scratch vectors (normalization_ids_scratch_, normalization_data_scratch_) are properly released via ReleaseNormalizationScratch() at appropriate boundaries. GetMemoryUsage() accounts for the new data structures.
Test coverage: The new tests cover all three quantization types (FP32/FP16/SQ8), dirty-run lifecycle (thresholds, boundaries, explicit normalization), serialization round-trips (both ordinary and streaming), heap equivalence (KNN/range, pure/filtered), and randomized differential testing. This is thorough.
One observation (not blocking): In Compact(), FinalizeInsertBatch() is called at the start, but Build() already calls NormalizeDirtyPostings() (which internally calls FinalizeInsertBatch()) immediately before Compact(). The second call is a no-op (dirty map is already empty), so this is harmless defensive coding.
No new blocking issues found. The existing review thread covers the remaining suggestions.
LHT129
left a comment
There was a problem hiding this comment.
I have reviewed the latest commit (25e45f8) which addresses the earlier review feedback from LHT129. The changes look good:
-
const_castremoval -cal_memory_usage()is nowconstandcurrent_memory_usage_ismutable std::atomic<uint64_t>. The serialization paths no longer needconst_cast. Clean solution. -
Suffix scan guard - The second
ScanPostingRangecall inquery_implis now guarded byif (selected.suffix_count > 0), avoiding the unnecessary function call for the common fully-normalized case. -
normalize_dirty_postings_for_serialization()extraction - The normalization logic is now factored into a dedicated helper, reducing duplication betweenSerializeandserialize_streaming_body. -
Test coverage - Comprehensive tests cover all three quantization types (FP32/FP16/SQ8), dirty-run thresholds and boundaries, KNN/range search with pure and filtered paths, serialization round-trips, randomized differential testing, and wire format version retention.
The core algorithm (binary-search partition of two sorted runs via SelectPostingRuns, merge-on-the-fly via ForEachSelectedPosting, and bounded normalization via FinalizeInsertBatch/NormalizeDirtyPostings) is sound. The performance data (3-10x Add throughput improvement) is compelling and the A/B testing confirms result-identical output.
No blocking issues found in the latest revision.
LHT129
left a comment
There was a problem hiding this comment.
Code Review Summary
I have completed a thorough review of this PR. The overall quality is high — the dirty posting mechanism is well-designed, the test coverage is comprehensive (98.28% changed-line coverage), and the existing review comments from LHT129 have addressed the main substantive issues.
Issues already identified and addressed by the author:
- const_cast elimination:
current_memory_usage_is nowmutable, andcal_memory_usage()isconst. ✅ - ScanPostingRange guard: The second
ScanPostingRangecall is now guarded byif (selected.suffix_count > 0). ✅ - Lock upgrade documentation: The exclusive lock in
Serialize/serialize_streaming_bodyis acknowledged as intentional. ✅
Remaining items for consideration:
[suggestion] normalize_dirty_postings_for_serialization() const-correctness
The method is declared const in both the header (sindi.h:360) and implementation (sindi.cpp:1578), but it calls window->NormalizeDirtyPostings() which is a non-const mutating method. While this works because const shared_ptr<T>& yields a non-const T* via operator->, the const qualifier is misleading to readers. The header comment already notes "The caller must hold global_mutex_" which partially addresses this. Consider either removing const from this method and its callers (Serialize, serialize_streaming_body), or adding a brief comment explaining the logical const pattern.
[note] FinalizeInsertBatch always returns true after processing
As LHT129 noted, when every pending term is missing from dirty_posting_prefixes_ (the continue branch at line 603), no posting state actually changed but the method still returns true. The caller in SINDI::Add() uses this return value to decide whether to call cal_memory_usage(). This is a minor inefficiency — cal_memory_usage() is cheap, so the impact is negligible.
[note] Redundant ReleaseNormalizationScratch() in NormalizeDirtyPostings
When FinalizeInsertBatch() already emptied dirty_posting_prefixes_, NormalizeDirtyPostings calls ReleaseNormalizationScratch() again (line 649) even though FinalizeInsertBatch already released it (line 633). The double-release is harmless (swap with already-empty vectors is a no-op), but slightly redundant.
What this PR does well:
- Clean separation of sorted prefix + unsorted dirty suffix with bounded thresholds (MIN=32, MAX=256)
- Binary partition (
SelectPostingRuns) correctly handles all edge cases including fully-dirty and fully-clean terms ForEachSelectedPostingtemplate enables efficient merged iteration for heap insertion- Memory accounting covers all new data structures
- Tests cover all three quantization types, serialization round-trips, filter correctness, and randomized stress scenarios
- Backward compatible wire format (version 1 retained)
LHT129
left a comment
There was a problem hiding this comment.
I have completed a thorough review of this PR. The overall code quality is high, with well-structured changes and comprehensive test coverage.
Summary of changes reviewed:
-
sparse_term_datacell.h/cpp— Core of the optimization. The two-run posting list design (sorted prefix + bounded dirty suffix) with binary-search partition (SelectPostingRuns) and two-pointer merge (NormalizePosting,ForEachSelectedPosting) is correct and well-implemented. TheFinalizeInsertBatch→NormalizeDirtyPostingslifecycle is properly integrated intoSINDI::Add,SINDI::Build, and serialization paths. -
sindi.cpp/h— Thecal_memory_usage()const-correctness fix andnormalize_dirty_postings_for_serialization()helper are clean. The exclusive lock upgrade in serialization is intentional and documented. -
inner_index_interface.h— Makingcurrent_memory_usage_mutable is appropriate since it is a cached value, not logical state. -
Tests — Excellent coverage: lifecycle tests (partial/full window, serialization boundaries),
SelectPostingRunscorrectness across all three quantization types, heap equivalence for KNN/range/filter paths, and a 20-seed randomized stress test. The test for serialization round-trip with dirty postings is particularly valuable.
Existing review comments from LHT129 have been addressed:
const_casteliminated via mutablecurrent_memory_usage_- Unnecessary
ScanPostingRangecall now guarded bysuffix_count > 0 FinalizeInsertBatchreturn value now accurately reflects whether changes occurred- Scratch vector allocation is now lazy (only after first dirty term found)
Remaining open items (already flagged by LHT129):
- Dead code after
CHECK_ARGUMENT(false, ...)inSelectPostingRunsandPostingEntryBefore— minor, non-blocking
The performance data in the PR description is thorough and convincing, with 1.6x–10x Add throughput improvements across all quantization types and batch sizes. The A/B verification confirms identical KNN/range/serialization results.
No new blocking issues found. The PR is in good shape.
LHT129
left a comment
There was a problem hiding this comment.
Thanks for this well-crafted optimization. The two-run posting list design with lazy normalization is a clean approach that delivers substantial incremental Add throughput improvements (up to 10x for single-vector batches) while preserving exact query semantics.
Summary of findings from this review pass:
The code quality is high. Most issues raised in earlier review rounds have been addressed:
const_castremoved viamutableoncurrent_memory_usage_and constcal_memory_usage()- Suffix scan guarded by
suffix_count > 0on the hot query path FinalizeInsertBatchreturn value now accurately reflects whether state changed- Scratch vectors lazily initialized only when needed
One minor note added:
FinalizeInsertBatch()is called twice when a window becomes full inSINDI::Add()(once directly, once insideNormalizeDirtyPostings()). The second call is a fast no-op but represents minor redundancy.
Unaddressed from prior reviews (LHT129, Aug 18):
- Dead code after
CHECK_ARGUMENT(false, ...)inSelectPostingRuns(line 152) andPostingEntryBefore(line 536). Thesereturnstatements are unreachable but harmless.
The test coverage is excellent — spanning all three quantization types, dirty-run thresholds, boundaries, serialization round-trips, randomized differential testing, and heap equivalence verification. The A/B benchmark methodology is rigorous and the results are compelling.
LHT129
left a comment
There was a problem hiding this comment.
Code Review Summary
I have completed a thorough review of this PR (commits 25e45f80 and d92c1973). The deferred posting-list normalization strategy is well-designed, and the implementation quality is high. The test coverage is comprehensive across all three quantization types (FP32, FP16, SQ8) with deterministic, randomized, lifecycle, serialization, and heap-equivalence coverage.
What was already addressed (from LHT129's prior review)
const_casteliminated by makingcurrent_memory_usage_mutableandcal_memory_usage()constFinalizeInsertBatch()now correctly returnsposting_state_changed(onlytruewhen actual work was done)- Scratch vectors (
order,sorted_ids,sorted_data) are lazily initialized insideFinalizeInsertBatch - Suffix scan in
query_implis guarded bysuffix_count > 0 - Documentation added for
normalize_dirty_postings_for_serializationexplaining the logical const contract
Remaining open items (from LHT129's latest review round)
-
[suggestion] Dead code after
CHECK_ARGUMENTinSelectPostingRuns(sparse_term_datacell.cpp:144): Thereturn {};is unreachable becauseCHECK_ARGUMENT(false, ...)always throws. Consider replacing with__builtin_unreachable()or removing it. -
[note] Dead code after
CHECK_ARGUMENTinPostingEntryBefore(sparse_term_datacell.cpp:146): Same pattern —return false;after the switch-defaultCHECK_ARGUMENT. Consider the same treatment for consistency. -
[note] Redundant
FinalizeInsertBatchcall inSINDI::Add()(sindi.cpp:409-411): When a window becomes full (total_count_ == window_size_),FinalizeInsertBatch()is called first at line 409, thenNormalizeDirtyPostings()at line 411 callsFinalizeInsertBatch()again internally. The second call is a fast no-op (emptypending_posting_terms_), but the redundancy is worth noting.
Additional observations
- The
SelectPostingRunsbinary search correctly partitions two sorted runs to find the top-k boundary. The edge cases (retained_count == 0,retained_count == posting_count, no dirty postings) are all handled correctly. - The
ForEachSelectedPostingmerge loop correctly interleaves prefix and suffix entries in sorted order, matching the semantics of the old single-run scan. - The exclusive lock change in
Serializeandserialize_streaming_bodyis an intentional and necessary trade-off to ensure postings are fully normalized before writing. This is well-documented in the PR description. NormalizeDirtyPostingsreturn value semantics are correct: returnstruewhen it actually performed normalization work,falsewhen there was nothing to normalize (including whenFinalizeInsertBatchalready handled everything).
Verdict
This is a well-executed optimization with impressive performance results (up to 10x throughput improvement for single-vector incremental Add). The remaining open items are all non-blocking [note]/[suggestion] level. No critical issues found.
LHT129
left a comment
There was a problem hiding this comment.
Overall this is a well-engineered optimization with thorough test coverage (98.28% changed-line coverage), comprehensive A/B benchmarks showing 1.57x–10.11x Add throughput improvements with no regression in search accuracy, and careful attention to serialization compatibility (wire version unchanged).
The core idea — deferring full posting-list normalization by maintaining a sorted prefix + bounded sorted dirty suffix — is sound and the merge-during-query approach preserves correctness. The lifecycle management (normalize at window-full, build boundary, compaction, and serialization) covers all necessary synchronization points.
Most of the detailed feedback has already been addressed in the existing review threads. The remaining open items are minor notes/suggestions that do not block merge.
LHT129
left a comment
There was a problem hiding this comment.
I completed a thorough review of all changed files in this PR. The existing reviews from LHT129 have already covered the key issues, and the author has addressed the important ones:
- const_cast removal —
cal_memory_usage()is nowconstandcurrent_memory_usage_ismutable. Clean solution. - suffix_count guard in query_impl — The hot-path
ScanPostingRangecall is now guarded byif (selected.suffix_count > 0). Good. - Exclusive lock in serialization — Confirmed as intentional design choice, documented in the PR description.
The core algorithm (SelectPostingRuns binary search + ForEachSelectedPosting merge) is correct and well-tested across all three quantization types with randomized coverage. The FinalizeInsertBatch threshold heuristic (clamp(prefix_count, 32, 256)) is reasonable.
The remaining open [note] items (dead code after CHECK_ARGUMENT, double FinalizeInsertBatch call in Add, int64_t loop variable) are minor and non-blocking.
No new issues found. The test coverage is comprehensive (deterministic lifecycle, serialization, filtering, boundary, randomized, and three-quantization regression tests).
LHT129
left a comment
There was a problem hiding this comment.
Reviewed the latest commit (d92c197). All three issues from the previous review round have been addressed:
-
const_cast removed —
cal_memory_usage()is nowconst,current_memory_usage_ismutable, andnormalize_dirty_postings_for_serializationisconstwith clear documentation explaining why physical mutation is safe under the caller-held exclusive lock. -
suffix_count > 0 guard added —
query_implnow guards the suffix scan withif (selected.suffix_count > 0). -
Lazy scratch allocation —
FinalizeInsertBatchusesstd::optional<Vector<>>for on-demand allocation of sort scratch buffers.
The implementation is solid:
- Two-run posting representation (sorted prefix + bounded dirty suffix) is well-designed
- Binary search partitioning in
SelectPostingRunscorrectly handles all edge cases - Merge-on-read during queries (
ForEachSelectedPosting,InsertHeapByTermLists) is correct - Lock discipline is sound: exclusive lock for serialization (intentional, documented), shared lock for queries (unchanged)
- Tests cover all three quantization types, dirty posting lifecycle, and serialization round-trip
- No correctness issues found
One minor observation (not blocking): FinalizeInsertBatch returns posting_state_changed = true even when a term only had its suffix sorted without a merge (prefix_count == 0 case). This triggers a harmless cal_memory_usage() recalculation since the posting list size hasn't changed. This is on the write path, not the hot query path, so the impact is negligible.
LHT129
left a comment
There was a problem hiding this comment.
This is a well-structured optimization with comprehensive test coverage and thorough benchmarking. The core idea of deferring full posting-list normalization by maintaining a sorted prefix + bounded sorted dirty suffix is sound.
The latest commit (d92c197) addresses several important review items: const_cast removal via mutable cache, lazy scratch-vector initialization with std::optional, correct FinalizeInsertBatch return value, and suffix-scan guard. The remaining open review threads (dead code after CHECK_ARGUMENT, int64_t loop variable, double FinalizeInsertBatch call in Add) are all [note] level and do not block merging.
The test coverage is excellent: deterministic lifecycle tests, serialization round-trip tests with all three quantization types, randomized differential tests, and heap-equivalence verification for both KNN and range search with filters. The A/B benchmarks show substantial incremental Add throughput improvements (1.6x-10x) with no regression in search accuracy or serialized output.
LHT129
left a comment
There was a problem hiding this comment.
Review Summary
This PR implements batch posting normalization for incremental SINDI Add, deferring full-list re-sorting and instead maintaining a sorted prefix plus a bounded dirty suffix. The merge-on-read strategy during query/pruning is sound, and normalization boundaries are correctly placed at full-window, build, compaction, and serialization points.
What was already reviewed and addressed
Previous review rounds by @LHT129 covered several items, all of which have been resolved or acknowledged:
const_castin serialization — Resolved by makingcurrent_memory_usage_mutableandcal_memory_usage()const.- Exclusive lock in
Serialize— Confirmed as intentional design choice for normalization-before-write. - Redundant
ScanPostingRangecall when no dirty postings — Fixed withif (selected.suffix_count > 0)guard. - Dead code after
CHECK_ARGUMENTinPostingEntryBefore— Noted as a defensive pattern consistent with the rest of the codebase. - Double
FinalizeInsertBatchcall inSINDI::Add()— Minor redundancy; the second call is a fast no-op. int64_tloop variable for container indexing — Style note; non-negative in practice.
Positive observations
- Algorithm correctness: The binary-search partition in
SelectPostingRunscorrectly finds the split point between two sorted runs. The merge logic inForEachSelectedPostingandNormalizePostingis consistent and correct. - Test coverage: Comprehensive — covers all three quantization types (FP32/FP16/SQ8), randomized stress tests with 20 seeds, serialization round-trips, heap equivalence (KNN + range, pure + filtered), and lifecycle boundaries.
- Performance: The benchmark data shows 1.6x–10x Add throughput improvement with no search regression, which is impressive.
- Wire compatibility: SINDI posting list format version remains 1; serialized bytes are unchanged.
- Memory: No material heap increase; scratch buffers are released after use via
ReleaseNormalizationScratch.
No new blocking or substantive issues found. The implementation is solid.
LHT129
left a comment
There was a problem hiding this comment.
This PR has already received a thorough review from LHT129 with 18 inline comments covering correctness, performance, code style, and maintainability concerns. The author has been responsive and addressed many of the initial findings in the second commit (d92c197).
The remaining 4 unresolved comments are all [suggestion]/[note] level:
- Dead code after
CHECK_ARGUMENTinSelectPostingRunsandPostingEntryBefore— minor style issue, safe as-is sinceCHECK_ARGUMENTalways throws. - Double
FinalizeInsertBatch()call when a window becomes full inSINDI::Add()— the second call is a fast no-op, no correctness impact. int64_tloop variable for indexingwindow_term_list_— minor type inconsistency, safe in practice since the index is derived fromcur_element_count_ / window_size_.
I have no additional blocking concerns to add. The PR is well-structured with comprehensive test coverage (unit tests, randomized stress tests, serialization round-trip tests across all three quantization types), thorough performance benchmarks, and clear documentation of the design decisions.
LHT129
left a comment
There was a problem hiding this comment.
I have completed a thorough review of this PR. The changes have already been well-reviewed by LHT129, and the author has addressed the key feedback items (const_cast removal, ScanPostingRange guard, exclusive lock justification).
Summary of my review:
The core approach — maintaining a sorted prefix plus a bounded dirty suffix to defer full-list re-sorting during incremental Add — is sound and well-motivated. The binary search in SelectPostingRuns correctly partitions two sorted runs, and the merge logic in ForEachSelectedPosting and NormalizePosting is correct.
The test coverage is excellent: the PR adds deterministic lifecycle tests, serialization round-trip tests with wire format version verification, filtering boundary tests, randomized differential tests across all three quantization types (FP32/FP16/SQ8), and heap equivalence tests for both KNN and range search with pure and filtered paths.
The performance data is thorough and convincing, with 1.6x-10x Add throughput improvements depending on batch size, and search latency within normal variation.
Remaining items from prior review (all [note] level):
- Dead code after
CHECK_ARGUMENT(false, ...)in bothSelectPostingRuns(return {};) andPostingEntryBefore(return false;) — these are unreachable but harmless. - Double
FinalizeInsertBatch()call inSINDI::Add()when a window becomes full — the second call is a fast no-op. int64_tloop variable for window indexing — minor type inconsistency.
None of these are blocking. The PR is in good shape for merging once the author decides on the remaining [note] items.
LHT129
left a comment
There was a problem hiding this comment.
I have completed a thorough review of this PR. The existing review comments from LHT129 have already covered the key points:
- The
const_castissue has been resolved by makingcurrent_memory_usage_mutable andcal_memory_usage()const. - The hot-path
suffix_count > 0guard has been added toquery_impl. - Dead-code-after-
CHECK_ARGUMENTpatterns inSelectPostingRunsandPostingEntryBeforehave been noted. - The redundant
FinalizeInsertBatchcall inAdd()when a window becomes full has been noted. - The signed loop variable for window indexing has been noted.
The PR is well-structured with comprehensive test coverage (deterministic lifecycle, serialization, filtering, boundary, randomized, and three-quantization regression tests). The performance data is thorough and convincing. The design of maintaining a sorted prefix + bounded sorted dirty suffix is sound, and the merge-during-pruning approach correctly preserves existing query semantics.
No new blocking issues found. The remaining open [note]-level comments are non-blocking and can be addressed at the author's discretion.
LHT129
left a comment
There was a problem hiding this comment.
This is a follow-up review after the second commit (d92c197). The previous review by @LHT129 already covered the key issues thoroughly, and most actionable suggestions have been addressed in this revision.
Summary of resolved issues from the first review round:
const_castremoved:current_memory_usage_is nowmutable,cal_memory_usage()isconst— clean.suffix_count == 0guard added inquery_implto skip the secondScanPostingRange— addressed.FinalizeInsertBatchnow returnstrueonly when at least one dirty term is processed — addressed.- Scratch vector allocation in
FinalizeInsertBatchis now deferred until the first valid dirty term — addressed. - Serialization exclusive lock is confirmed as intentional and documented.
Remaining open notes from @LHT129 (non-blocking):
- Dead code after
CHECK_ARGUMENTinSelectPostingRuns(return {};) andPostingEntryBefore(return false;). FinalizeInsertBatchcalled twice when a window becomes full inSINDI::Add()(once directly, once insideNormalizeDirtyPostings).int64_t windowloop variable vssize_tcontainer index.
My assessment:
The core algorithm — maintaining a sorted prefix + bounded dirty suffix, merging on-the-fly during queries, and normalizing at boundaries — is sound. The two-run merge logic in SelectPostingRuns, ForEachSelectedPosting, and NormalizePosting is correct. Test coverage is extensive (deterministic lifecycle, serialization round-trips, randomized differential tests across all three quantization types). The performance data in the PR description is thorough and shows significant incremental Add throughput improvements with no regression in search accuracy.
No new issues found beyond what has already been noted. The remaining notes are minor and do not block merge.
LHT129
left a comment
There was a problem hiding this comment.
I have completed a thorough review of the latest commit (d92c197). The previous round of review comments has been well addressed:
- The
const_castin serialization paths has been eliminated by makingcal_memory_usage()const andcurrent_memory_usage_mutable, with normalization factored into a dedicatednormalize_dirty_postings_for_serialization()helper. - The hot-path
ScanPostingRangecall is now guarded byif (selected.suffix_count > 0), avoiding the no-op function call for fully normalized postings. - The exclusive lock in serialization is an intentional design choice documented in the PR description.
The remaining open notes (dead code after CHECK_ARGUMENT, redundant FinalizeInsertBatch call in the full-window path, and int64_t loop variable) are minor and non-blocking.
The core algorithm — binary-search partitioning of two sorted posting runs, merge-based normalization, and bounded dirty-suffix thresholds — is correct and well-tested with comprehensive coverage across all three quantization types, randomized seeds, and lifecycle boundaries. No new issues found.
LHT129
left a comment
There was a problem hiding this comment.
This is the third review round for this PR. The second commit (d92c197) addressed 5 out of 9 issues from the previous review rounds:
Resolved:
const_casteliminated —current_memory_usage_mademutable,cal_memory_usage()madeconst✅ScanPostingRangesuffix guard added (if (selected.suffix_count > 0)) ✅FinalizeInsertBatchnow returnsposting_state_changed(onlytruewhen at least one dirty term is processed) ✅- Scratch vectors (
order,sorted_ids,sorted_data) are now lazily allocated viastd::optional✅ normalize_dirty_postings_for_serializationconst documentation improved ✅
Remaining (all [note] level, previously flagged by LHT129):
PostingEntryBeforedead code afterCHECK_ARGUMENT(sparse_term_datacell.cpp:538)SelectPostingRunsdead code afterCHECK_ARGUMENT(sparse_term_datacell.cpp:152-153)FinalizeInsertBatchcalled twice inSINDI::Add()— once directly, then insideNormalizeDirtyPostings()(sindi.cpp:409/411)int64_tloop variable for container indexing (sindi.cpp:408)
These are all minor style issues that do not affect correctness or performance. No new issues found in this review round.
Overall assessment: The implementation is solid. The core algorithm (deferred batch posting normalization with bounded dirty suffix) is correctly implemented with proper binary search partitioning, two-way merge, and threshold-triggered normalization. Test coverage is thorough across FP32/FP16/SQ8 quantization types, including randomized differential tests. Serialization correctly normalizes dirty state before writing. Memory accounting covers all new data structures.
LGTM.
LHT129
left a comment
There was a problem hiding this comment.
[note] In Compact(), the newly created compact_pending_flags vector is zero-initialized without copying the existing pending_posting_flags_ values. While FinalizeInsertBatch() is called at the start of Compact() and clears flags for terms in pending_posting_terms_, there is a subtle edge case: if any terms exist in dirty_posting_prefixes_ but were not tracked in pending_posting_terms_ (the "unprocessed insert batch" scenario), their corresponding flags would be silently dropped after compaction. The current callers are safe because SINDI::Build() calls NormalizeDirtyPostings() before Compact(), but future callers of Compact() may not be aware of this precondition. Consider either copying the existing flags into compact_pending_flags, or documenting that Compact() expects dirty postings to already be normalized.
d92c197 to
299628d
Compare
Keep each incrementally updated posting list as two sorted runs, merge those runs during search, and normalize only when a run reaches its threshold or persistence requires canonical order. Apply the same behavior to SINDI and SINDI V2 with focused correctness and lifecycle coverage. Signed-off-by: charlesxu91 <charlesxu.mi@gmail.com> Assisted-by: Codex:gpt-5.5
299628d to
8c2b3bb
Compare
Change Type
Linked Issue
What Changed
Addno longer re-sorts the full posting list after every batch.Test Evidence
make fmtequivalent with clang-format 15.0.7make lintequivalent on changed production sources with clang-tidy 15.0.7make testequivalent: completeunittestssuite on Linux x86-64make covequivalent, run focused tests, and collect changed-line coverageTest details:
Focused coverage includes all sparse value encodings (
fp32,fp16,sq8), dirty-run thresholds and boundaries, 20 randomized seeds, partial/full/next-window incremental lifecycles, KNN top-k 1/10/100, range search, pure/filter paths (100%, 50%, 1% selectivity), serialization/streaming serialization, restore, wire version retention, and Add after restore.The four uncovered added executable lines are defensive unreachable paths: the binary-partition failure guard, the invalid-quantization guard, and the no-dirty-posting early return.
Compatibility Impact
Performance and Concurrency Impact
Controlled A/B setup
main(0dde8e4b9fe9a5cc643a4ee4f3a23a29fe0c1a70).nice +10,OPENBLAS_NUM_THREADS=1,OMP_NUM_THREADS=1, network disabled.baseline current current baseline baseline current current baseline baseline currentfor FP32 (5 samples/version); the first six entries for FP16/SQ8 (3 samples/version).Incremental Add throughput
Across these cells, Add batch p95 latency falls by 27.0% to 88.7%.
Search, accuracy, serialization, and memory
c8c02d5c436e7561, rangea8791bd19b32cc97, serializedec785c19cea62734, 10,398,147 bytes.eba60050ecc56435, range1ffdf5c7871487ff, serialized658e543a8b68023a, 7,905,646 bytes.7224cdf82ecda77b, rangeab7ebf6bbfe94ba2, serialized5beb0893264ad66d, 6,662,690 bytes.GetMemoryUsage()misses vectors added after a window is created, so it is not used for the A/B memory conclusion. Linuxmallinfo2heap differs by only +0.12% to +0.23%. The full matrix observed point-in-time RSS changes of +4.6 to +4.8 MiB; a separate order-balanced FP32/batch-256 breakdown (5 processes/version) measured median RSS +1.39 MiB, comprising only +0.02 MiB anonymous RSS and +1.37 MiB file-backed RSS. This indicates no material retained-heap increase in the measured workload.Documentation Impact
README.mdDEVELOPMENT.mdCONTRIBUTING.mdThe optimization does not add or change a user-facing parameter or workflow.
Risk and Rollback
Add.Checklist
[skip ci]prefix)