Skip to content

[diskann-inmem] Add support for Product Quantization - #1469

Open
Mark Hildebrand (hildebrandmw) wants to merge 7 commits into
mainfrom
mhildebr/inmem-pq-2
Open

Mark Hildebrand (hildebrandmw) wants to merge 7 commits into
mainfrom
mhildebr/inmem-pq-2

Conversation

@hildebrandmw

@hildebrandmw Mark Hildebrand (hildebrandmw) commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Add support for PQ for diskann-inmem. This follows the work in #1458 and #1432.

For reviewers, this PR is broken into 6 commits:

Commit 1

Adds a with_capacity constructor to ObjectPool to create an initially empty pool. This is used for distance table buffering in the PQ repr.

Commit 2

Factors out Reranker to repr/internal/quantization/rerank.rs. This will be shared between spherical and PQ. In addition, an internal Metric enum is introduced for the three currently supported metrics: SquaredL2, InnerProduct, and Cosine.

I kept spherical::Rerank as a public version of the internal Rerank enum to try to decouple the public configuration API from how things are done on the back end. Yes, this is a little bit of code duplication, but keeps the API looking like

Spherical::config(
   ...
   spherical::Rerank::F16, // <-- comes from `spherical`.
)

The amount of duplication is small, keeps the interface consistent, and lets us experiment on the back end a little.

Additionally, the routines for test_expand_beam and test_prune are moved out of the spherical test module and into repr/test/mod.rs. These will be reused by the PQ representation.

Commit 3

Adds a generic Quantized representation. The extension point is a [Quantizer] trait which contains methods for quantizing data and creating distance computers. Distance computation uses BYOTE to allow implementations to create multiple specialized computers as they see fit. These specialized computers are inlined directly into the ExpandBeam and Prune trait objects used for search and prune respectively.

The goal behind this approach is to make it easy to implement scalar, minmax, and binary quantizers (since we just need to implement the Quantizer trait) and to make development of new quantizers easy. And hopefully BYOTE means that the performance cost of this is minimal.

One final advantage is that the Quantized representation is a single concrete type, minimizing monomorphization bloat of this approach.

Commit 4

This actually adds the PQ Quantizer.

Query distances are done using the usual trick of pre-populating a lookup table with distances between query chunk fragments and the PQ pivots (see #1458 for the implementation). Since these tables are fairly large (~200KB for a 192 PQ chunks), an ObjectPool is used to reuse allocations. The configuration for this pool in Config is such that an unbounded ObjectPool is not allowed.

Quant-quant distances use the PaddedTable introduced in #1458.

Tests are similar to spherical, but significantly easier since PQ can losslessly represent the test dataset.

Commit 5

Adds integration test support for PQ and baselines covering L2, inner-product, and cosine.

Commit 6

Adds support in diskann-benchmark for the new PQ backend.

Benchmarks

The infrastructure in #1458 pays off. This is considerably faster than the PQ index in diskann-providers. A benchmarking input JSON is included in the comment - highlights are reported here.

Dataset Version PQ Chunks Build Time (s) QPS (L = 50) QPS (L = 100)
wiki-100k current 192 33.6 11028 (0.858) 7884 (0.924)
wiki-100k PR 192 11.4 21570 (0.859) 15653 (0.922)
msturing-100k current 25 5.61 43739 (0.732) 25417 (0.837)
msturing-100k PR 25 2.32 76639 (0.728) 41245 (0.841)
oai-v3-100k current 384 96 2382 (0.957) 1469 (0.977)
oai-v3-100k PR 384 41 9440 (0.956) 5710 (0.977)

Note that the performance increase is somewhat exaggerated due to the small size of the dataset (less memory bandwidth because more fits in cache), which highlights perf differences that a bandwidth-constrained environment would hide.

@hildebrandmw
Mark Hildebrand (hildebrandmw) added this pull request to stack #1468 October 7, 2026 00:03
Base automatically changed from mhildebr/basic-table to main October 8, 2026 18:18
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83867% with 65 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.03%. Comparing base (18c9cce) to head (1cac598).

Files with missing lines Patch % Lines
diskann-benchmark/src/index/inmem2.rs 85.98% 22 Missing ⚠️
...iskann-inmem/src/repr/internal/quantization/mod.rs 45.45% 18 Missing ⚠️
diskann-inmem/src/repr/quantized/mod.rs 98.55% 9 Missing ⚠️
diskann-inmem/integration/index/runner.rs 88.88% 8 Missing ⚠️
diskann-inmem/src/repr/quantized/product.rs 98.78% 5 Missing ⚠️
...ann-inmem/src/repr/internal/quantization/rerank.rs 98.03% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1469      +/-   ##
==========================================
+ Coverage   90.94%   92.03%   +1.08%     
==========================================
  Files         586      590       +4     
  Lines      118391   119785    +1394     
==========================================
+ Hits       107675   110243    +2568     
+ Misses      10716     9542    -1174     
Flag Coverage Δ
miri 92.03% <95.83%> (+1.08%) ⬆️
unittests 91.97% <95.83%> (+1.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
diskann-inmem/integration/index/object.rs 99.00% <ø> (ø)
diskann-inmem/integration/main.rs 82.05% <100.00%> (+0.71%) ⬆️
diskann-inmem/src/repr/internal/mod.rs 100.00% <ø> (ø)
diskann-inmem/src/repr/mod.rs 93.75% <ø> (ø)
diskann-inmem/src/repr/spherical.rs 97.43% <100.00%> (-0.05%) ⬇️
diskann-inmem/src/repr/test/mod.rs 94.77% <100.00%> (+4.77%) ⬆️
diskann-utils/src/object_pool.rs 99.60% <100.00%> (+0.02%) ⬆️
...ann-inmem/src/repr/internal/quantization/rerank.rs 98.03% <98.03%> (ø)
diskann-inmem/src/repr/quantized/product.rs 98.78% <98.78%> (ø)
diskann-inmem/integration/index/runner.rs 92.30% <88.88%> (+0.45%) ⬆️
... and 3 more

... and 50 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hildebrandmw
Mark Hildebrand (hildebrandmw) marked this pull request as ready for review October 9, 2026 16:52
@hildebrandmw
Mark Hildebrand (hildebrandmw) requested review from a team and a balanced review from Copilot October 9, 2026 16:52
@hildebrandmw

Copy link
Copy Markdown
Contributor Author
Benchmarking JSON
{
  "search_directories": [
    "redacted"
  ],
  "output_directory": null,
  "jobs": [
    {
      "type": "inmem2",
      "content": {
        "build": {
          "alpha": 1.2,
          "l_build": 50,
          "max_degree": 42,
          "num_threads": 8,
          "pruned_degree": 32
        },
        "data": {
          "data": "wikipedia/wikipedia_base_100K.bin",
          "data_type": "float32",
          "distance": "inner_product"
        },
        "search": {
          "groundtruth": "wikipedia/wikipedia-100k.gt.bin",
          "num_threads": [
            8
          ],
          "queries": "wikipedia/wikipedia_query.bin",
          "reps": 3,
          "runs": [
            {
              "recall_k": 10,
              "search_l": [
                10,
                20,
                30,
                40,
                50,
                60,
                70,
                80,
                90,
                100
              ],
              "search_n": 10
            }
          ]
        },
        "quantization": {
          "product": {
            "chunks": 192,
            "centers": 256,
            "seed": 1
          }
        }
      }
    },
    {
      "type": "inmem2",
      "content": {
        "build": {
          "alpha": 1.2,
          "l_build": 50,
          "max_degree": 42,
          "num_threads": 8,
          "pruned_degree": 32
        },
        "data": {
          "data": "msturing/msturing-base-100k.fbin",
          "data_type": "float32",
          "distance": "squared_l2"
        },
        "search": {
          "groundtruth": "msturing/msturing-100k.gt.10k.bin",
          "num_threads": [
            8
          ],
          "queries": "msturing/queries-10k.fbin",
          "reps": 3,
          "runs": [
            {
              "recall_k": 10,
              "search_l": [
                10,
                20,
                30,
                40,
                50,
                60,
                70,
                80,
                90,
                100
              ],
              "search_n": 10
            }
          ]
        },
        "quantization": {
          "product": {
            "chunks": 25,
            "centers": 256,
            "seed": 1
          }
        }
      }
    },
    {
      "type": "inmem2",
      "content": {
        "build": {
          "alpha": 1.2,
          "l_build": 50,
          "max_degree": 42,
          "num_threads": 8,
          "pruned_degree": 32
        },
        "data": {
          "data": "openai-v3/base-100k.bin",
          "data_type": "float32",
          "distance": "cosine"
        },
        "search": {
          "groundtruth": "openai-v3/gt-100k.bin",
          "num_threads": [
            8
          ],
          "queries": "openai-v3/query.bin",
          "reps": 3,
          "runs": [
            {
              "recall_k": 10,
              "search_l": [
                10,
                20,
                30,
                40,
                50,
                60,
                70,
                80,
                90,
                100
              ],
              "search_n": 10
            }
          ]
        },
        "quantization": {
          "product": {
            "chunks": 384,
            "centers": 256,
            "seed": 1
          }
        }
      }
    },
    {
      "type": "graph-index-build-pq",
      "content": {
        "index_operation": {
          "search_phase": {
            "groundtruth": "wikipedia/wikipedia-100k.gt.bin",
            "num_threads": [
              8
            ],
            "queries": "wikipedia/wikipedia_query.bin",
            "reps": 3,
            "runs": [
              {
                "recall_k": 10,
                "search_l": [
                  10,
                  20,
                  30,
                  40,
                  50,
                  60,
                  70,
                  80,
                  90,
                  100
                ],
                "search_n": 10
              }
            ],
            "search-type": "topk"
          },
          "source": {
            "alpha": 1.2000000476837158,
            "backedge_ratio": 1.0,
            "data": "wikipedia/wikipedia_base_100K.bin",
            "data_type": "float32",
            "distance": "inner_product",
            "index-source": "Build",
            "insert_retry": null,
            "l_build": 50,
            "max_degree": 32,
            "multi_insert": null,
            "num_threads": 8,
            "save_path": null,
            "start_point_strategy": "medoid"
          }
        },
        "max_fp_vecs_per_prune": 0,
        "num_pq_chunks": 196,
        "seed": 13076402859301299683,
        "use_fp_for_search": false
      }
    },
    {
      "type": "graph-index-build-pq",
      "content": {
        "index_operation": {
          "search_phase": {
            "groundtruth": "msturing/msturing-100k.gt.10k.bin",
            "num_threads": [
              8
            ],
            "queries": "msturing/queries-10k.fbin",
            "reps": 3,
            "runs": [
              {
                "recall_k": 10,
                "search_l": [
                  10,
                  20,
                  30,
                  40,
                  50,
                  60,
                  70,
                  80,
                  90,
                  100
                ],
                "search_n": 10
              }
            ],
            "search-type": "topk"
          },
          "source": {
            "alpha": 1.2000000476837158,
            "backedge_ratio": 1.0,
            "data": "msturing/msturing-base-100k.fbin",
            "data_type": "float32",
            "distance": "squared_l2",
            "index-source": "Build",
            "insert_retry": null,
            "l_build": 50,
            "max_degree": 32,
            "multi_insert": null,
            "num_threads": 8,
            "save_path": null,
            "start_point_strategy": "medoid"
          }
        },
        "max_fp_vecs_per_prune": 0,
        "num_pq_chunks": 25,
        "seed": 13076402859301299683,
        "use_fp_for_search": false
      }
    },
    {
      "type": "graph-index-build-pq",
      "content": {
        "index_operation": {
          "search_phase": {
            "groundtruth": "openai-v3/gt-100k.bin",
            "num_threads": [
              8
            ],
            "queries": "openai-v3/query.bin",
            "reps": 3,
            "runs": [
              {
                "recall_k": 10,
                "search_l": [
                  10,
                  20,
                  30,
                  40,
                  50,
                  60,
                  70,
                  80,
                  90,
                  100
                ],
                "search_n": 10
              }
            ],
            "search-type": "topk"
          },
          "source": {
            "alpha": 1.2000000476837158,
            "backedge_ratio": 1.0,
            "data": "openai-v3/base-100k.bin",
            "data_type": "float32",
            "distance": "cosine",
            "index-source": "Build",
            "insert_retry": null,
            "l_build": 50,
            "max_degree": 32,
            "multi_insert": null,
            "num_threads": 8,
            "save_path": null,
            "start_point_strategy": "medoid"
          }
        },
        "max_fp_vecs_per_prune": 0,
        "num_pq_chunks": 384,
        "seed": 13076402859301299683,
        "use_fp_for_search": false
      }
    }
  ]
}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A duplicated match arm can fail warnings-as-errors Clippy checks, alongside several documentation and diagnostic issues.

5 open findings
What changed in this PR

Adds Product Quantization support to diskann-inmem, including shared reranking infrastructure, integration coverage, and benchmarking.

Changes:

  • Implements PQ search, insertion, pruning, metrics, reranking, and distance-table pooling.
  • Refactors spherical quantization to share internal metric and reranking logic.
  • Adds integration baselines and benchmark support.
File Description
diskann-utils/​src/​object_pool.rs Adds empty capacity-bounded pool construction.
diskann-inmem/​src/​repr/​test/​mod.rs Shares quantized representation test helpers.
diskann-inmem/​src/​repr/​spherical.rs Adopts shared quantization internals.
diskann-inmem/​src/​repr/​product.rs Implements the PQ representation.
diskann-inmem/​src/​repr/​mod.rs Exports PQ APIs.
diskann-inmem/​src/​repr/​internal/​quantization/​rerank.rs Adds shared reranking logic.
diskann-inmem/​src/​repr/​internal/​quantization/​mod.rs Adds internal metric conversions.
diskann-inmem/​src/​repr/​internal/​mod.rs Registers quantization internals.
diskann-inmem/​integration/​main.rs Registers PQ regression tests.
diskann-inmem/​integration/​jsons/​graph/​product/​l2.json Configures L2 PQ tests.
diskann-inmem/​integration/​jsons/​graph/​product/​l2-baseline.json Records L2 baselines.
diskann-inmem/​integration/​jsons/​graph/​product/​ip.json Configures inner-product PQ tests.
diskann-inmem/​integration/​jsons/​graph/​product/​ip-baseline.json Records inner-product baselines.
diskann-inmem/​integration/​jsons/​graph/​product/​cosine.json Configures cosine PQ tests.
diskann-inmem/​integration/​jsons/​graph/​product/​cosine-baseline.json Records cosine baselines.
diskann-inmem/​integration/​index/​runner.rs Builds PQ integration indexes.
diskann-inmem/​integration/​index/​object.rs Enables PQ index dispatch.
diskann-benchmark/​src/​index/​inmem2.rs Adds PQ benchmark training and execution.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread diskann-inmem/src/repr/product.rs Outdated
Comment thread diskann-inmem/integration/index/runner.rs
Comment thread diskann-inmem/src/repr/product.rs Outdated
Comment thread diskann-inmem/src/repr/product.rs Outdated
Comment thread diskann-inmem/src/repr/test/mod.rs Outdated
@hildebrandmw
Mark Hildebrand (hildebrandmw) force-pushed the mhildebr/inmem-pq-2 branch 2 times, most recently from 514ba3a to 4711c41 Compare October 9, 2026 20:01
@hildebrandmw
Mark Hildebrand (hildebrandmw) marked this pull request as draft October 12, 2026 22:42
@hildebrandmw
Mark Hildebrand (hildebrandmw) marked this pull request as ready for review October 12, 2026 22:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The PQ constructor currently accepts tables with more centers than its byte-sized encoding can represent.

1 open finding
5 resolved since last review

🧠 Review effort: Balanced

Comment thread diskann-inmem/src/repr/quantized/product.rs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants