Skip to content

Commit 3110fd2

Browse files
Evandabestmeta-codesync[bot]
authored andcommitted
Add MetalIndexIVFPQ with product quantization and residual encoding support (#5288)
Summary: Adds IVF-PQ (inverted file with product quantization) index support to the Metal GPU backend - Add `MetalIndexIVFPQ` with full train/add/search/reset/copyFrom/copyTo support - Add `MetalIVFPQImpl` GPU-resident IVF list storage for PQ codes (segment allocator, same pattern as IVFFlat) - Support 8-bit product quantization with precomputed per-query lookup tables - Support both L2 and inner product metrics - Residual encoding when `by_residual=true` (default for L2) - CPU-side PQ lookup table computation with precomputed tables optimization for L2 - GPU scan path via `runMetalIVFPQFullSearch` with CPU LUT fallback via `runMetalIVFPQScan` - Update `MetalCloner` to support IVFPQ in `index_cpu_to_metal_gpu` / `index_metal_gpu_to_cpu` ## Changes - **New:** `MetalIndexIVFPQ.h/.mm` - IVFPQ index class (train, add, search, reset, copyFrom/copyTo, cloner support) - **New:** `impl/MetalIVFPQ.h/.mm` - GPU-resident IVF list storage with segment allocator for PQ codes - **New:** `test/TestMetalIndexIVFPQ.mm` - 4 C++ tests (L2, IP, reset, CPU↔GPU round-trip) - **Modified:** `test/CMakeLists.txt` - added TestMetalIndexIVFPQ build target ## Differences from CUDA IVFPQ **Training:** Delegates to CPU (`IndexIVFPQ::train`). CUDA can train on GPU. Same rationale as IVFFlat - training is a one-time cost. **Add path:** Coarse quantization and PQ encoding run on CPU, then codes are copied to GPU storage. CUDA does both on GPU. On Apple Silicon with unified memory, the copy cost is minimal. **Residual encoding:** When `by_residual=true`, residuals (x - coarse_centroid) are computed on CPU before PQ encoding. CUDA computes residuals on GPU. Functionally equivalent. **Lookup tables:** PQ distance lookup tables are computed on CPU and uploaded to GPU for the scan phase. CUDA computes LUTs on GPU. CPU LUT computation is fast relative to the scan and avoids a separate GPU kernel launch. **IVF list storage:** Same segment allocator pattern as IVFFlat - single contiguous buffer rather than CUDA's per-list `DeviceVector` allocations. ## Note FP16 coarse quantizer and GPU merge kernel are planned optimizations for a future PR. Both apply across all IVF index types (IVFFlat, IVFPQ, IVFSQ). ## Build and test ```bash cmake -B build \ -DFAISS_ENABLE_GPU=OFF \ -DFAISS_ENABLE_METAL=ON \ -DBUILD_TESTING=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="$(brew --prefix libomp)" \ . cmake --build build --target faiss faiss_metal TestMetalIndexIVFPQ -j$(sysctl -n hw.logicalcpu) cd build && ctest -R TestMetalIndexIVFPQ --output-on-failure ``` Pull Request resolved: #5288 Reviewed By: alibeklfc Differential Revision: D113037852 Pulled By: mnorris11 fbshipit-source-id: 5fdb729b10ad9884a673fc3483543b4c5fcdb621
1 parent 3b52da3 commit 3110fd2

11 files changed

Lines changed: 1197 additions & 8 deletions

File tree

.github/actions/build_cmake/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ runs:
247247
-DCMAKE_BUILD_TYPE=Release \
248248
-DCMAKE_PREFIX_PATH="$(brew --prefix libomp)" \
249249
.
250-
cmake --build build --target faiss faiss_metal swigfaiss TestMetalIndexFlat TestMetalIndexIVFFlat -j$(sysctl -n hw.logicalcpu)
250+
cmake --build build --target faiss faiss_metal swigfaiss TestMetalIndexFlat TestMetalIndexIVFFlat TestMetalIndexIVFPQ -j$(sysctl -n hw.logicalcpu)
251251
- name: C++ tests
252252
if: inputs.metal != 'ON'
253253
shell: bash

faiss/gpu_metal/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ set(FAISS_METAL_SRC
1616
MetalFlatKernels.mm
1717
MetalIndexFlat.mm
1818
MetalIndexIVFFlat.mm
19+
MetalIndexIVFPQ.mm
1920
impl/MetalIVFFlat.mm
21+
impl/MetalIVFPQ.mm
2022
StandardMetalResources.mm
2123
MetalCloner.mm
2224
MetalPythonBridge.mm

faiss/gpu_metal/MetalCloner.mm

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
#import "MetalCloner.h"
1010
#include <faiss/IndexFlat.h>
1111
#include <faiss/IndexIVFFlat.h>
12+
#include <faiss/IndexIVFPQ.h>
1213
#include <faiss/impl/FaissAssert.h>
1314
#include <cstring>
1415
#import "MetalIndexFlat.h"
1516
#import "MetalIndexIVFFlat.h"
17+
#import "MetalIndexIVFPQ.h"
1618
#import "StandardMetalResources.h"
1719

1820
namespace faiss {
@@ -35,6 +37,14 @@ int get_num_gpus() {
3537
MetalIndexConfig config;
3638
config.device = 0;
3739

40+
const auto* ivfPQ = dynamic_cast<const faiss::IndexIVFPQ*>(index);
41+
if (ivfPQ) {
42+
FAISS_THROW_IF_NOT(
43+
ivfPQ->metric_type == METRIC_L2 ||
44+
ivfPQ->metric_type == METRIC_INNER_PRODUCT);
45+
return new MetalIndexIVFPQ(res->getResources(), ivfPQ, config);
46+
}
47+
3848
const auto* ivfFlat = dynamic_cast<const faiss::IndexIVFFlat*>(index);
3949
if (ivfFlat) {
4050
FAISS_THROW_IF_NOT(
@@ -63,10 +73,26 @@ int get_num_gpus() {
6373

6474
FAISS_THROW_MSG(
6575
"index_cpu_to_metal_gpu: unsupported index type "
66-
"(supported: IndexFlat, IndexIVFFlat)");
76+
"(supported: IndexFlat, IndexIVFFlat, IndexIVFPQ)");
6777
}
6878

6979
faiss::Index* index_metal_gpu_to_cpu(const faiss::Index* index) {
80+
const auto* metalIvfPQ = dynamic_cast<const MetalIndexIVFPQ*>(index);
81+
if (metalIvfPQ) {
82+
auto* quantizer = (metalIvfPQ->metric_type == METRIC_INNER_PRODUCT)
83+
? (faiss::IndexFlat*)new faiss::IndexFlatIP(metalIvfPQ->d)
84+
: (faiss::IndexFlat*)new faiss::IndexFlatL2(metalIvfPQ->d);
85+
auto* cpu = new faiss::IndexIVFPQ(
86+
quantizer,
87+
metalIvfPQ->d,
88+
metalIvfPQ->nlist(),
89+
metalIvfPQ->getNumSubQuantizers(),
90+
8);
91+
cpu->own_fields = true;
92+
metalIvfPQ->copyTo(cpu);
93+
return cpu;
94+
}
95+
7096
const auto* metalIvf = dynamic_cast<const MetalIndexIVFFlat*>(index);
7197
if (metalIvf) {
7298
auto* quantizer = (metalIvf->metric_type == METRIC_INNER_PRODUCT)
@@ -91,7 +117,7 @@ int get_num_gpus() {
91117

92118
FAISS_THROW_MSG(
93119
"index_metal_gpu_to_cpu: unsupported index type "
94-
"(supported: MetalIndexFlat, MetalIndexIVFFlat)");
120+
"(supported: MetalIndexFlat, MetalIndexIVFFlat, MetalIndexIVFPQ)");
95121
}
96122

97123
} // namespace gpu_metal

faiss/gpu_metal/MetalIndexIVFFlat.h

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,8 @@
2121

2222
namespace faiss {
2323
namespace gpu_metal {
24-
class MetalIVFFlatImpl;
25-
} // namespace gpu_metal
26-
} // namespace faiss
2724

28-
namespace faiss {
29-
namespace gpu_metal {
25+
class MetalIVFFlatImpl;
3026

3127
/// IVFFlat index wrapper for Metal backend.
3228
/// Currently delegates to an internal CPU IndexIVFFlat; later phases

faiss/gpu_metal/MetalIndexIVFPQ.h

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// @lint-ignore-every LICENSELINT
2+
/**
3+
* Copyright (c) Meta Platforms, Inc. and its affiliates.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
* Metal IVF-PQ index: 8-bit product quantization with precomputed
9+
* per-query lookup tables and GPU IVF list scanning.
10+
*/
11+
12+
#pragma once
13+
14+
#import <Metal/Metal.h>
15+
16+
#include <faiss/IndexIVFPQ.h>
17+
#include <faiss/gpu_metal/MetalIndex.h>
18+
19+
#include <memory>
20+
21+
namespace faiss {
22+
namespace gpu_metal {
23+
24+
class MetalIVFPQImpl;
25+
26+
class MetalIndexIVFPQ : public MetalIndex {
27+
public:
28+
MetalIndexIVFPQ(
29+
std::shared_ptr<MetalResources> resources,
30+
int dims,
31+
idx_t nlist,
32+
int M,
33+
int nbitsPerIdx,
34+
faiss::MetricType metric,
35+
float metricArg = 0.0f,
36+
MetalIndexConfig config = MetalIndexConfig());
37+
38+
MetalIndexIVFPQ(
39+
std::shared_ptr<MetalResources> resources,
40+
const faiss::IndexIVFPQ* cpuIndex,
41+
MetalIndexConfig config = MetalIndexConfig());
42+
43+
~MetalIndexIVFPQ() override;
44+
45+
void train(idx_t n, const float* x) override;
46+
void add(idx_t n, const float* x) override;
47+
void add_with_ids(idx_t n, const float* x, const idx_t* xids) override;
48+
void reset() override;
49+
50+
void search(
51+
idx_t n,
52+
const float* x,
53+
idx_t k,
54+
float* distances,
55+
idx_t* labels,
56+
const SearchParameters* params = nullptr) const override;
57+
58+
void copyFrom(const faiss::IndexIVFPQ* index);
59+
void copyTo(faiss::IndexIVFPQ* index) const;
60+
61+
void updateQuantizer();
62+
std::vector<idx_t> getListIndices(idx_t listId) const;
63+
void reclaimMemory();
64+
65+
/// Pre-allocate GPU storage for the given total number of vectors.
66+
void reserveMemory(idx_t numVecs);
67+
68+
idx_t nlist() const;
69+
size_t nprobe() const;
70+
int getNumSubQuantizers() const;
71+
void setUsePrecomputedTables(bool enable);
72+
bool getUsePrecomputedTables() const;
73+
74+
private:
75+
std::unique_ptr<faiss::IndexIVFPQ> cpuIndex_;
76+
std::unique_ptr<MetalIVFPQImpl> gpuIvf_;
77+
78+
void verifyPQSettings_() const;
79+
80+
void encodeResidualAndAppend_(
81+
idx_t n,
82+
const float* x,
83+
const idx_t* list_nos,
84+
const idx_t* xids);
85+
};
86+
87+
} // namespace gpu_metal
88+
} // namespace faiss

0 commit comments

Comments
 (0)