Migrate disk PQ flat scan to flat API - #1341
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
a802e20 to
4d78a62
Compare
There was a problem hiding this comment.
Pull request overview
This PR migrates the disk PQ “flat scan” path onto the shared diskann::flat API, introducing a dedicated disk PQ FlatSearchStrategy + visitor that scans PQ-compressed rows and then reuses the existing full-precision reranking + filtering pipeline. It also factors PQ query preprocessing into a reusable owned query-computer (TransposedQueryComputer) so both graph and flat PQ search can share the same preprocessing approach.
Changes:
- Update
FlatIndex::knn_searchto return a lifetime-boundSendFutureso it can borrow strategy/context/output across.await. - Add
TransposedQueryComputer(+ error type) to build per-query PQ lookup tables for transposed PQ tables. - Route disk flat scan through
FlatIndexusing a new disk-specific flat strategy/visitor, and remove now-unused PQ scratch batching API.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| diskann/src/flat/index.rs | Adjusts knn_search signature/lifetimes to support borrowed-provider flat search entrypoints. |
| diskann-quantization/src/product/tables/transposed/query.rs | Introduces an owned PQ query computer for transposed tables (L2/IP), with unit tests. |
| diskann-quantization/src/product/tables/transposed/mod.rs | Wires the new transposed query module into the transposed table submodule exports. |
| diskann-quantization/src/product/tables/mod.rs | Re-exports the new transposed query computer + error at the tables module boundary. |
| diskann-quantization/src/product/mod.rs | Re-exports the new transposed query types at the product module boundary. |
| diskann-disk/src/search/provider/disk_provider.rs | Implements disk PQ flat scan via diskann::flat (DiskFlatProvider/DiskFlatSearchStrategy/DiskFlatVisitor) while preserving scan-time filtering and rerank behavior. |
| diskann-disk/src/search/pq/pq_scratch.rs | Removes PQScratch::max_vectors and updates tests accordingly (no longer needed after migration). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Aditya Krishnan (@arkrishn94) I ended up making a few design changes beyond the
One related detail: filtering happens in These were the main areas where the migration required broader architectural choices, so feedback on them would be helpful before finalizing the approach. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1341 +/- ##
==========================================
- Coverage 91.55% 91.54% -0.02%
==========================================
Files 521 521
Lines 100371 100609 +238
==========================================
+ Hits 91899 92098 +199
- Misses 8472 8511 +39
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mark Hildebrand (hildebrandmw)
left a comment
There was a problem hiding this comment.
As usual, I will defer to the maintainers of diskann-disk to make the judgement calls here, but what immediately stands out to me is that trying to fit the flat scan into the diskann flat-scan API is essentially recreating the custom flat-scan implementation but with significantly more code. That is, this appears to be working hard to fit the API (and indeed changing the API in diskann) without materially benefiting from doing so.
To me, this indicates two things:
- There is an ergonomic gap in the flat API that needs to be fixed. For example - it requires a
QueryComputerwhich is causing some of the churn in this PR [1]. I don't think that's a good direction since it separates the compute engine from the internal of theFlatAccessor, when closer coupling (e.g. howSearchAccessorworks now for the graph index) allows for safer optimization. - We're missing even lower-level infrastructure (e.g. generic batch PQ computation independent of
diskann-disk) that would help with reusability. Think: a more generally reuseable version ofcompute_pq_distance.
There are parts that look good. Extracting rerank_and_filter to a synchronous function (instead of the current unfortunate bounce through async) is a good improvement. Simplifying PQ scratch initialization is good - though I might suggest keeping it in DiskSearchScratch fusing it with the DiskSearchScratch's pooled API to avoid the multi-stage initialization that is currently done.
[1] The graph portion of diskann used to work this way and it turns out to be way better for a huge number of reasons to not.
|
I agree with your assessment. This migration exposed a limitation in the current flat API: separating the visitor from I also agree that the better direction is to improve the flat API itself. Following the principle established in PR #1067, I propose making flat visitors query-aware and responsible for producing distances. Proposed APIpub trait DistancesUnordered: HasId + Send + Sync {
type Error: ToRanked + Debug + Send + Sync + 'static;
fn distances_unordered<F>(
&mut self,
f: F,
) -> impl SendFuture<Result<(), Self::Error>>
where
F: Send + FnMut(Self::Id, f32);
}
pub trait SearchStrategy<'a, P, T>: Send + Sync
where
P: DataProvider,
{
type Visitor: DistancesUnordered<Id = P::InternalId>;
type Error: StandardError;
fn create_visitor(
&'a self,
provider: &'a P,
context: &'a P::Context,
query: T,
) -> Result<Self::Visitor, Self::Error>;
}The generic flat-search flow becomes: let mut visitor = strategy.create_visitor(provider, context, query)?;
visitor.distances_unordered(callback).await?;
processor.post_process(&mut visitor, query, candidates, output).await?;The responsibility boundary would be:
For disk PQ, graph and flat search can then use one pooled The main advantages are:
The main trade-off is a public flat-trait change. To limit migration cost, the existing trait and method names remain. I also searched for visible consumers and did not find an independent public implementation outside DiskANN itself, forks, and vendored copies. I have tried this proposal in the latest revision of the PR so that the design can be reviewed through a concrete implementation:
I also agree that a generic batch PQ primitive independent of I would appreciate your review of both the proposed API direction and the implementation in this revision. Does this align with what you had in mind? Mark Hildebrand (@hildebrandmw) Aditya Krishnan (@arkrishn94) |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
flat::knn_searchentry point.Reference Issues/PRs
Closes #1104. The query-aware ownership direction follows the graph-search accessor model discussed in #1067.
What does this implement/fix? Briefly explain your changes.
flat::knn_searchentry point while retainingFlatIndex::knn_searchas a convenience wrapper.DiskSearchScratchbetween graph and flat PQ search.The previous flat API separated visitors from query-specific distance computation. For disk PQ, that split required query state to be spread across multiple objects, pools, and initialization stages. Making the visitor query-aware lets each backend combine data access and distance computation while the generic flat layer continues to own top-k selection, comparison accounting, error escalation, and post-processing.
Any other comments?
Validation includes targeted flat framework, PQ scratch, and disk filtered-search tests; graph and flat PQ search, filtering, pooled-query isolation, and indexed-vector coverage; and workspace clippy with warnings denied.