[diskann-inmem] Prepare code for quantization and beyond - #1352
Draft
Mark Hildebrand (hildebrandmw) wants to merge 33 commits into
Draft
[diskann-inmem] Prepare code for quantization and beyond#1352Mark Hildebrand (hildebrandmw) wants to merge 33 commits into
Mark Hildebrand (hildebrandmw) wants to merge 33 commits into
Conversation
added 30 commits
July 29, 2026 15:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prepare
diskann-inmemfor quantization and beyond.Note that this is infrastructure work to get the code ready. Quantization is not yet integrated.
Goals
The requirements to support quantization are annoyingly orthogonal:
Collections like PQ, scalar, and spherical quantization should be able to run in "quant-only" mode and "quant + full-precision" mode, where the full-precision store is used for reranking. Internally, I would like these to be the same type to cut down on unnecessary monomorphization. This means we need support for at least two collections managed by the same epoch protected
Store(one quant, the other full-precision). In this case, the full-precision store can do without the invasive tags used for the primary store, but also needs to be optional and ideally support any off32,f16,u8,i8and beyond.For testing purposes, we probably want to retain the ability for PQ to do hybrid pruning (part full-precision, part quantized). This completely breaks the current model used by inmem of managing raw
&[u8]slices. While we could technically make it work, if we needed to do something like multi-vector operations, a&[u8]is just not the right approach anyways.We also want to be able to support multi-vectors and other kinds of non-uniform data in an epoch guarded
Store, which gets rid of the uniform assumption of the current invasive store.Supporting all of these required a pretty drastic reorganization of
diskann-inmem.Architecture
This PR is all about moving things up and down. The architecture went from this:
where the provider had a separate
LayerandStoreand combined the two to make aSearchAccessorandPruneAccessorto thisFrom top to bottom:
The
Providernow contains aLayerinstead of aLayerand aStore. Instead, theStorehas been moved directly into theLayer. Instead of theProviderbeing responsible for building aSearchAccessorfrom pieces exposed by the oldLayer, theProvidercompletely delegatesSearchAccessorandPruneAccessorconstruction to itsLayer.The rest of the
Provider's job is interfacing thediskann::graph::glueAPI to the simplifiedLayerAPI.Layernow gains theStore. TheLayerfamily of traits is extended to include logical operations like insert and retire. As mentions above, it is also responsible for buildingSearchAccessors andPruneAccessors.Storereceives minor changes - its internalBufferwhere it used to manage the invasive data store directly has now been moved to aPlugintrait. In this architecture, theStoreis just responsible for driving thePlugintrait's lifecycle API and is completely uninvolved with the mechanics of raw data reading and writing.Pluginis a new trait for a slot-based store whose slots are driven byStore. The old invasive store is an example of such aPlugin.In addition to this hierarchical layering, a
Layeris allowed (and indeed, expected) to bypass its immediateStoreto the underlyingPlugindirectly to build the various accessors. For theInvasivestore, this works because concurrency tags are embedded directly in thePlugin- readers do not need anything to do with the parent store.Why This Mostly (Probably) Works
The
Plugintrait provides an extension point for managing different types of data. For the quantized case, we reuseInvasivefor the quantized data and a simplerBuffer-based one for the full-precision reranking data. Then we can createPluginconsisting of both layers. The short-cut from aLayerto itsPluginmeans a quantized provider can create it'sSearchAccessor/PruneAccessors with knowledge of both. We could even have anInvasivestore for both the quantized data and the full-precision data and reuse the existing full-precision infrastructure to support both quantized and full-precision searches over the same `Layer.Additionally,
Pluginmakes no requirements on the kind of data store. This allows us to store un-even sized allocations in aPlugin. It's theLayer's job to make sense of everything.Finally, the
Layerknowing the details of itsPlugins means we can (with some creativity) still support hybrid pruning.Suggested Reviewing Order
This is a large PR, but I tried very hard to keep things structured. The reviewing order outlined here is a suggested bottom-up order. Understanding how the lower levels work is important for understanding how the higher ones come together.
num.rs: A quick warm-up. This PR introduces some strongly typed integers with specific semantics.prefetch.rs: Another warm-up. I wasn't satisfied with the safety/flexibility of prefetching in the current in-mem provider. This PR exacerbated the situation, so I introduced a bit more structure on prefetchers.store/plugin.rs: This defines thePlugintrait and it's expected lifecycle. I captured the nuances in healthy module level and trait level documentation. This is probably the most nuanced change in this PR.store/checked.rs: An implementation ofPluginthat aggressively checks that the invariants required for thePluginAPI are upheld byStore. Again, there is healthy module-level documentation describing the logic.store/invasive.rs: The old invasive data store moved to implement thePlugintrait. This largely preserves what was already in the oldStoreand is conceptually much simpler thanstore/checked.rs.store/mod.rs: ModifyingStoreto work against aPlugingeneric instead of directly managing the invasive store. Note that there are some changes to initialization.Store::newnow takes three distinct arguments:Layout: Description for capacity, number of frozen points, and maximum degree.Config: Configuration state dedicated directly to management of the internal concurrency data structures.PluginConfig: Configuration of the internal plugin. TheXConfigtraits are used in this PR to perform deferred initialization of large data structure.Detour: With the introduction of a modularized
Store, changes were made to the following integration-test related files to enable concurrent stress-test of differentStoreimplemntations.src/integration/store/*: Shared boiler plate and implementations for exposing differentStoreto the integration test framework. This PR exposes wrappers for theInvasiveandCheckedstores in theinvasiveandcheckedmodules respectively.integration/store/*: Integration test exposure for the different stores as different jobs in the integration test suite.For these, the overall structure of the exposed stores is extremely similar. Most of churn in these files is moving things around so keep the amount of repeated code to a minimum.
layer/mod.rs: ReworkedLayer/Set/Search/Inserttraits for the new architecture.Layer: Gains a few life-cycle related items.Set: Is now responsible for also obtaining an internal slot for the inserted element.Search/Insert: Reworked to returnSearchAccessorandPruneAccessors directly. This is mainly to allow us to keep the internalStore/Plugindetails hidden from the public interface.In addition:
ExpandBeam: Moves from its old location inprovider.rs. Otherwise, is mostly unchanged.Prune: A new trait for pruning. Prune implementation now internally "buffer" items in the prune set. This effectively makes the type of the elements being pruned hidden, allowing for hybrid pruning in the future.layer/full.rs: This is where everything comes together. TheConfigis used to group together full-precision related constructor arguments andFullnow gains aStore. Importantly, now thatFullknows the full details of its store, we can more aggressively optimizeExpandBeamwith fewer bounds and length checks. Note that start point initialization is now managed byFull's constructor.The implementation of
ExpandBeamis taken pretty much directly from the oldprovider.rscode. Additionally, the tests have gotten more robust with Miri having more coverage of theExpandBeamimplementation and correctness tests for the various distance specializations.AI Disclosure: An agent was used review changes and implementation details, help brainstorm, and make focused edits to documentation.