DiskSeismic: add scalar-quantized index (disk_seismic_sq) - #37
Conversation
Signed-off-by: Zirui Song <zrsong@amazon.com>
2d3cfce to
df101bc
Compare
| sq_(quantizer_type, vmin, vmax), | ||
| cluster_parameter_(parameter) {} | ||
|
|
||
| void DiskSeismicScalarQuantizedIndex::read_csr(const char* file_path, |
There was a problem hiding this comment.
do you need to override this? I think you only want to handle kMmap use case
There was a problem hiding this comment.
Yes — the override exists only to reject the kMmap case: a mapped CSR is borrowed as float, but this index searches over codes, so mapping it would misread. The in-memory case just delegates to the base. So it's scoped to exactly the kMmap use case you mention. DiskSeismicIndex doesn't need it (it's float), and this mirrors SeismicScalarQuantizedIndex::read_csr. Kept.
| // A quantizer described by an index file. bytes_per_value() treats anything but | ||
| // QT_8bit as 16-bit, so an undefined type would silently pick an element width | ||
| // rather than be rejected. | ||
| ScalarQuantizer quantizer_from_file(QuantizerType type, float vmin, |
There was a problem hiding this comment.
while it's named "quantizer from file", but the signature has nothing to do with file
There was a problem hiding this comment.
Good catch — renamed to make_scalar_quantizer(type, vmin, vmax);
| } | ||
|
|
||
| // A candidate block: its summary score and its (posting list, cluster) address. | ||
| struct BlockCandidate { |
There was a problem hiding this comment.
it's shared with diskseismicindex, reuse
| // Score every document of one block against the dense query codes, dedup via | ||
| // `visited` and honor the id selector. Vectors come from the inline forward | ||
| // index (fwd) when loaded, else from the in-RAM CSR of a fresh build. | ||
| void score_block(const detail::InlineForwardIndex* fwd, |
There was a problem hiding this comment.
seems these helper functions share the logic with DiskSeismicIndex, could you refactor them to make them work for both indices?
| // Resolve `cut` and `k_prime`. A DiskSeismicSearchParameters (including a | ||
| // DiskSeismicSQSearchParameters) carries k_prime; a plain | ||
| // SeismicSearchParameters (or null) uses the default budget. | ||
| const DiskSeismicSearchParameters defaults; |
There was a problem hiding this comment.
duplicate logic block, make it shared. Check even if search logic can be refactored to a shared function
| query_sq.encode(values, codes.data(), nnz); | ||
| const uint8_t* query_values = codes.data(); | ||
|
|
||
| std::vector<std::vector<float>> result_distances( |
There was a problem hiding this comment.
share code with line 204,205
|
|
||
| auto DiskSeismicScalarQuantizedIndex::single_query( | ||
| std::vector<uint8_t>& dense, absl::flat_hash_set<idx_t>& visited, | ||
| const term_t* q_idx, const uint8_t* q_val_bytes, size_t q_len, |
There was a problem hiding this comment.
expand q_xxx to query_xxx
| } | ||
|
|
||
| void DiskSeismicScalarQuantizedIndex::write_index(IOWriter* io_writer) { | ||
| write_header(io_writer); |
There was a problem hiding this comment.
change to write_quantization_header
There was a problem hiding this comment.
Renamed to write_quantization_header. One note: the sibling SeismicScalarQuantizedIndex (from #36) calls its equivalent write_quantizer_header — want me to rename that one to match, or should I keep write_quantizer_header here for parity with SESQ? Happy either way; just want them consistent.
There was a problem hiding this comment.
sorry, please make them consistent
|
|
||
| DiskSeismicScalarQuantizedIndex* DiskSeismicScalarQuantizedIndex::mmap_index( | ||
| int dimension, const char* index_file, size_t pos) { | ||
| throw_if_null(index_file, "index_file must not be null"); |
There was a problem hiding this comment.
there is a valid file check in check.h, use that for file check, you may also want to apply in DiskSeismicIndex
There was a problem hiding this comment.
I'm using throw_if_null(index_file, ...) from checks.h here, same as SeismicIndex/InvertedIndex/DiskSeismicIndex::mmap_index; the file's openability is then enforced by MmapFile's constructor, which throws if it can't open. I didn't spot a stronger file-validity helper in checks.h (it has null/positive/overflow checks). Did you have a specific one in mind — or want me to add a shared throw_if_invalid_index_file and apply it across all the mmap readers? Happy to do that consistently.
There was a problem hiding this comment.
interesting, I remember I implemented a check for file pointer and whether file exist, but it's not there, I guess it's removed somehow or in a local branch, never mind.
9fa616b to
a5110f9
Compare
| return {cut, k_prime}; | ||
| } | ||
|
|
||
| pair_of_score_id_vectors_t padded_results(idx_t n, int k) { |
There was a problem hiding this comment.
what about initialize_padded_results
There was a problem hiding this comment.
Renamed to initialize_padded_results.
| n, std::vector<idx_t>(k, INVALID_IDX))}; | ||
| } | ||
|
|
||
| pair_of_score_id_vector_t groc_search_query( |
There was a problem hiding this comment.
groc is not a common term here, right?
There was a problem hiding this comment.
You are right. Renamed the function to block_budget_query and dropped "GroC" from the comments.
| query_values + i * element_size, element_size, | ||
| dense + static_cast<size_t>(query_indices[i]) * element_size); | ||
| } | ||
| visited.clear(); |
There was a problem hiding this comment.
the function will only be called once per query, right?
There was a problem hiding this comment.
Yes — one call per query inside the #pragma omp for.
chishui
left a comment
There was a problem hiding this comment.
write_index/read_index/mmap_index/add/buildand the whole batch-search loop are still near-verbatim copies of DiskSeismicIndex's, differing only in the value width and the quantizer header. Extract a shared base instead of keeping a third parallel copy.- Same for the tests -- both the C++ helpers and the python file are forks of the disk_seismic ones.
write_quantization_headervs SESQ'swrite_quantizer_headeris still inconsistent.
| // summary (stored at the same width), so scores are comparable across | ||
| // posting lists for the global ranking. | ||
| std::vector<BlockCandidate> candidates; | ||
| std::vector<float> score_scratch; |
There was a problem hiding this comment.
both allocate per query on the hot path. pass them in as per-thread scratch like dense/visited, or at least reserve
There was a problem hiding this comment.
Moved candidates and score_scratch out to per-thread scratch, passed in alongside dense/visited, so nothing allocates per query now.
| "read_index(file, IndexIoFlag::kUseMmap)"); | ||
| } | ||
|
|
||
| void DiskSeismicScalarQuantizedIndex::write_quantization_header( |
There was a problem hiding this comment.
SeismicScalarQuantizedIndex still has write_quantizer_header, please rename that one too
There was a problem hiding this comment.
Done — renamed SESQ's write_quantizer_header and read_quantizer_header to write_quantization_header/read_quantization_header so both indexes match.
| inv_list_writer.mmap_deserialize(&cursor); | ||
| detail::InlineForwardIndex forward; | ||
| forward.mmap_deserialize(&cursor); | ||
| throw_if_element_size_mismatch(forward, sq); |
There was a problem hiding this comment.
this only checks fwd. score_summaries_transposed dispatches on the summaries' own element_size_, verify that against the quantizer too?
There was a problem hiding this comment.
Good catch — added an element_size() accessor on InvertedListClusters and validate_mapped_payload now checks the summaries' width against the quantizer in addition to the forward index's.
| return results; | ||
| } | ||
|
|
||
| void DiskSeismicScalarQuantizedIndex::write_index(IOWriter* io_writer) { |
There was a problem hiding this comment.
this and read_index/mmap_index/add/build are still copies of DiskSeismicIndex's, differing only in the code width and the quantizer header. shared base?
There was a problem hiding this comment.
Done — added DiskSeismicIndexBase (disk_seismic_index_base.{h,cpp}). It owns add/build/search/write_index/read_index and the mmap payload load; both indexes derive from it and supply only the differences via hooks (code_element_size, encode_values, encode_query, decode_scores, write_payload_header, validate_mapped_payload). The two .cpps dropped ~730 lines between them. DiskSeismic's bit-exact parity tests still pass, so behavior is unchanged.
|
|
||
| void DiskSeismicScalarQuantizedIndex::write_index(IOWriter* io_writer) { | ||
| write_quantization_header(io_writer); | ||
| const uint64_t nv = num_vectors_; |
There was a problem hiding this comment.
nit: drop the const instead of const_cast-ing it away. same in disk_seismic_index.cpp
There was a problem hiding this comment.
Dropped the const on nv instead of casting it away, here and in disk_seismic_index.cpp
| query_sq.encode(values, codes.data(), nnz); | ||
| const uint8_t* query_batch = codes.data(); | ||
|
|
||
| pair_of_score_id_vectors_t results = detail::padded_results(n, k); |
There was a problem hiding this comment.
every row is overwritten below, so the -1.0/INVALID_IDX fill is n*k wasted writes on this path
| } | ||
|
|
||
| if (index_type == "disk_seismic_sq") { | ||
| std::string quantizer_str = get_param("quantizer", "8bit"); |
There was a problem hiding this comment.
- duplicate of the
seismic_sqblock below, move the quantizer/vmin/vmax/cluster parsing into a function - an unknown
quantizer=value silently becomes 8bit, reject it? - FYI the
seismic_sqcopy has.lambda = lambda = lambda(line 157) -- a shared helper kills that too
There was a problem hiding this comment.
- Extracted parse_cluster_params and parse_quantizer_config, shared by seismic/disk_seismic/seismic_sq/disk_seismic_sq.
- An unrecognized quantizer= now throws instead of defaulting to 8bit (test added).
- That also removed the .lambda = lambda = lambda self-assignments.
| // Use %factory to enable proper downcasting based on runtime type | ||
| %newobject nsparse::index_factory; | ||
| %factory(nsparse::Index* nsparse::index_factory, nsparse::BrutalIndex, nsparse::SeismicIndex, nsparse::SeismicScalarQuantizedIndex, nsparse::IDMapIndex, nsparse::InvertedIndex); | ||
| %factory(nsparse::Index* nsparse::index_factory, nsparse::BrutalIndex, nsparse::SeismicIndex, nsparse::SeismicScalarQuantizedIndex, nsparse::DiskSeismicScalarQuantizedIndex, nsparse::IDMapIndex, nsparse::InvertedIndex); |
There was a problem hiding this comment.
nsparse::DiskSeismicIndex is missing from both %factory lists, add it?
There was a problem hiding this comment.
Added nsparse::DiskSeismicIndex to both %factory lists.
a5110f9 to
d5c8204
Compare
Signed-off-by: Zirui Song <zrsong@amazon.com>
d5c8204 to
891d9a0
Compare
Thanks — I think this predated my last push. Here's where each stands on the latest revision:
|
Description
This PR is to implement the scalar quantization of DiskSeismicIndex.
Issues Resolved
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.