Repository navigation
[multi-vector] Add MinMax MaxSim kernel factory - #1508
juchen-ms (partychen) wants to merge 2 commits into
Conversation
Integrate the merged MinMax matrix kernel with an owned-query factory, runtime ISA selection, an independent scalar reference, and shared construction errors. Preserve QueryMatRef inputs and add focused factory and query-cloning coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 94d30f43-fadc-4a6b-889b-406de424a1ab
There was a problem hiding this comment.
🔵 Needs a closer look
Unsafe storage cloning and runtime-selected SIMD paths warrant human review with cross-platform validation results, which were not verified here.
No open issues 🎉
What changed in this PR
Integrates the MinMax matrix kernel into DiskANN’s multi-vector distance API through an owned-query factory, preserving QueryMatRef inputs.
Changes:
- Adds runtime backend selection, prepared kernels, and an independent scalar reference.
- Introduces shared construction errors and owned MinMax query cloning.
- Adds coverage for bit widths, backend layouts, ownership, validation, and edge-case scores.
| File | Description |
|---|---|
diskann-quantization/src/multi_vector/distance/factory.rs |
Adds overflow to shared construction errors. |
diskann-quantization/src/minmax/multi/reference.rs |
Adds an independent scalar scoring baseline. |
diskann-quantization/src/minmax/multi/prepared.rs |
Wraps packed kernels with validation and distance conversion. |
diskann-quantization/src/minmax/multi/mod.rs |
Registers and exports the new factory interfaces. |
diskann-quantization/src/minmax/multi/meta.rs |
Implements owned query cloning and cloning tests. |
diskann-quantization/src/minmax/multi/kernel.rs |
Defines kernel and erasure traits with shared validation. |
diskann-quantization/src/minmax/multi/factory.rs |
Implements backend selection, construction checks, and factory tests. |
diskann-quantization/src/minmax/mod.rs |
Exports the public MinMax factory API. |
diskann-quantization/src/matrix_kernels/maxsim/packed_minmax_x_unpacked_minmax.rs |
Exposes the accumulator limit for factory validation. |
diskann-quantization/src/matrix_kernels/maxsim/mod.rs |
Removes the integrated kernel’s dead-code suppression. |
Review effort: Balanced
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1508 +/- ##
==========================================
- Coverage 92.16% 91.08% -1.09%
==========================================
Files 592 596 +4
Lines 123582 124073 +491
==========================================
- Hits 113901 113006 -895
- Misses 9681 11067 +1386
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Mark Hildebrand (hildebrandmw)
left a comment
There was a problem hiding this comment.
Thanks for working on this. I think trying to follow the pattern established by the full-precision kernels is doubling down on patterns we should be trying to fix instead.
Instead, I would encourage you to take an approach of (1) returning a concrete object from the factory APIs and (2) seeing if you can find an approach that is more ergonomic to use and implement than the complexity somewhat inherent in the factory design of the full precision kernels. Bonus points if you can figure out a way to tie the full-precision and min-max abstractions together (though don't feel the need to do so, particularly if makes the interfaces less clear).
|
Thanks for the review, Mark Hildebrand (@hildebrandmw). Updated in 09f3d47: the factory now returns I tried sharing the full-precision and MinMax abstractions, but the existing interface is tied to |
Replace caller-provided erasure with a concrete MinMaxMaxSim object and library-owned constructors for each bit-width pair. Select fixed-layout kernels through RuntimeArch and retain the independent scalar reference with rowmajor-owned query storage. Keep the low-level matrix kernel unchanged. Centralize canonical view validation, use explicit bit-width names, and preserve the existing typed query and document inputs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 94d30f43-fadc-4a6b-889b-406de424a1ab
09f3d47 to
023d2dd
Compare
Mark Hildebrand (hildebrandmw)
left a comment
There was a problem hiding this comment.
Thanks - this is significantly cleaner, but now it forces us to address the compilation cost of these kernels.
The easiest thing to do is to just compile the one kernel we care about. Thinking more broadly, though, about the problem is about specializing the entire loopnest for all bit combinations and all architectures compounds the code generation cost with probably a dubious ROI for that specialization.
Thinking abstractly, the most important bit to specialize is probably at the panel kernel level. Maybe a tiny bit higher. Everything else could be moved behind dynamically dispatched interfaces (maybe), which would avoid some of the cost. I don't know how all that would end up working out, though. Just a thought.
| ) -> Result<(), MaxSimError>; | ||
|
|
||
| #[cfg(test)] | ||
| fn type_id(&self) -> std::any::TypeId { |
There was a problem hiding this comment.
It's generally a good idea to call these like inner_type_id or something. type_id can get too easily conflated with std::any::Any::type_id, which (in the case of a trait object for example), would accidentally return the type ID of Box<dyn Trait> instead of the type-id of the wrapped type.
| }) | ||
| .sum(); | ||
| let b = doc.meta(); | ||
| let ip = ((q.a * b.a) * dot as f32 + q.n * b.b) |
There was a problem hiding this comment.
There should really be a shared helper for this arithmetic instead of duplicating it in multiple places.
| factory!($a, 8, ScalarWords, V3Words, V4Words); | ||
| )+}; | ||
| } | ||
| factory!(1, 2, 3, 4, 5, 6, 7, 8); |
There was a problem hiding this comment.
This is what I was worried was hiding under the previous generic interface. With all of these now being compiled into diskann-quantization, the time for a release compilation of the crate goes from around 7 seconds on my machine to 23 seconds! That's a 3x increase.
That's way too much. If you are set on materializing all these bit specializations, something needs to be done on the backend to avoid monomorphizing the entire stack for all bits, especially if the logical underlying code remains the same.
Otherwise, please just pick the one that we actually care about to try to limit code bloat.
This also ties into my concern on this line of work about the impact on test code generation. Test code is usually run at much lower optimization level, but testing all of these has a compilation cost as well.
Reference Issues/PRs
Follow-up to #1494, which introduced the standalone MinMax matrix kernel.
What does this implement/fix? Briefly explain your changes.
Expose the MinMax matrix kernel through
build_minmax_max_sim, returning a concreteMinMaxMaxSim<B_BITS>that owns the prepared query and can be reused across documents.QueryMatRef<MinMaxMeta<A_BITS>>inputs, with independent query/document widths from 1 through 8.diskann-quantization, removing the caller-provided visitor andBoxEraseargument.rowmajor::Owned<u8>.BuildMaxSimError, withOverflowfor storage-size failures.No new dependencies. Existing full-precision factory signatures and pairwise APIs remain unchanged.
Usage example: MinMax8 query against MinMax4 documents
The query and documents are already quantized and have matching intrinsic dimensions. Build once, then reuse the kernel and score buffer:
For an owned query, pass
QueryMatRef::from(query.as_view()). Each call overwritesscores; the example clones it only to retain each document’s result. Lower scores indicate greater similarity.Any other comments?
The updated usage example was compile-checked against the current API. Tests cover all 64 bit-width combinations, panel tails, backend/layout selection, query ownership, invalid inputs, nonfinite scores and storage overflow. See the PR checks for current CI results.