Faiss(14):IndexIVFPQ的CPU search过程分析

本文解析了Faiss中IndexIVFPQ在CPU上的搜索过程,包括Python接口调用、量化器搜索、预分配搜索等关键步骤,并深入探讨了search_preassigned函数的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 说明

之前分析过了faiss 在GPU中的search过程,这里分析一下IndexIVFPQ在CPU中的search过程,即不将index拷贝到GPU中。

2. 过程分析

2.1 python接口

CPU search的python接口与GPU的完全一致,没有差别。

D, I = gpu_index.search(xq_t[x],top_k)

2.2 faiss core

IndexIVF::search

因为IndexIVFPQ没有override search,所以在实际运行过程中会调用父类IndexIVF中的实现。

void IndexIVF::search (idx_t n, const float *x, idx_t k,
                         float *distances, idx_t *labels) const
{
    std::unique_ptr<idx_t[]> idx(new idx_t[n * nprobe]);
    std::unique_ptr<float[]> coarse_dis(new float[n * nprobe]);

    // 1. quantizer中的粗量搜索
    quantizer->search (n, x, nprobe, coarse_dis.get(), idx.get());

    // 2. prefetch_lists函数为空
    invlists->prefetch_lists (idx.get(), n * nprobe);

    // 3. 预分配搜索
    search_preassigned (n, x, k, idx.get(), coarse_dis.get(),
                        distances, labels, false);
}

search过程主要包含三个部分:

  1. quantizer->search
    量化器搜索聚类中心,该过程在quantizer实例中进行,计算所有(nlist个)聚类中心与每一条搜索向量的距离,并从中找出最近的nprobe个聚类中心,输出到idx和coarse_dis中。
    所以这里的idx和coarse_dis是大小为 nnprobe 的二维数组,分别存放nnprobe个向量label和与原向量的距离。

  2. invlists->prefetch_lists
    这里没有用到这类功能,所以实际调用的该函数为空。

  3. search_preassigned
    经过第一步计算出粗聚类中心向量后,在这里进行二次计算,即在选定的聚类里再次计算出top_k个近邻向量。由于在add时已经对向量进行了预计算形成残差,所以这里只要进行向量和运算就可以了。
    在实际测试时发现,search的主要时间消耗是在这里,占比达到96%以上,所以针对这一过程进行进一步分析。

IndexIVF::search_preassigned

    /** search a set of vectors, that are pre-quantized by the IVF
     *  quantizer. Fill in the corresponding heaps with the query
     *  results. The default implementation uses InvertedListScanners
     *  to do the search.
     *
     * @param n      nb of vectors to query
     * @param x      query vectors, size nx * d
     * @param assign coarse quantization indices, size nx * nprobe
     * @param centroid_dis
     *               distances to coarse centroids, size nx * nprobe
     * @param distance
     *               output distances, size n * k
     * @param labels output labels, size n * k
     * @param store_pairs store inv list index + inv list offset
     *                     instead in upper/lower 32 bit of result,
     *                     instead of ids (used for reranking).
     * @param params used to override the object's search parameters
     */
void IndexIVF::search_preassigned (idx_t n, const float *x, idx_t k,
                                   const idx_t *keys,
                                   const float *coarse_dis ,
                                   float *distances, idx_t *labels,
                                   bool store_pairs,
                                   const IVFSearchParameters *params) const
{
    // max_codes是默认值0
    long nprobe = params ? params->nprobe : this->nprobe;
    long max_codes = params ? params->max_codes : this->max_codes;

    size_t nlistv = 0, ndis = 0, nheap = 0;

    // 根据计算类型定义堆
    using HeapForIP = CMin<float, idx_t>;
    using HeapForL2 = CMax<float, idx_t>;

    bool interrupt = false;

    // don't start parallel section if single query
    bool do_parallel =
        parallel_mode == 0 ? n > 1 :
        parallel_mode == 1 ? nprobe > 1 :
        nprobe * n > 1;

#pragma omp parallel if(do_parallel) reduction(+: nlistv, ndis, nheap)
    {
        InvertedListScanner *scanner = get_InvertedListScanner(store_pairs);
        ScopeDeleter1<InvertedListScanner> del(scanner);

        /****************************************************
         * Actual loops, depending on parallel_mode
         ****************************************************/

        if (parallel_mode == 0) {

#pragma omp for
            for (size_t i = 0; i < n; i++) {

                if (interrupt) {
                    continue;
                }
                
                // 在inverted_list中设置搜索的起始点
                scanner->set_query (x + i * d);
                // 根据i设置distances和labels的地址
                float * simi = distances + i * k;
                idx_t * idxi = labels + i * k;

                init_result (simi, idxi);

                long nscan = 0;

                // 依次在nprobe个聚类中进行搜索
                for (size_t ik = 0; ik < nprobe; ik++) {

                    nscan += scan_one_list (
                         keys [i * nprobe + ik],
                         coarse_dis[i * nprobe + ik],
                         simi, idxi
                    );

                    if (max_codes && nscan >= max_codes) {
                        break;
                    }
                }

                ndis += nscan;
                //对搜索结果进行排序
                reorder_result (simi, idxi);

                if (InterruptCallback::is_interrupted ()) {
                    interrupt = true;
                }

            } // parallel for
        } else if (parallel_mode == 1) {
            std::vector <idx_t> local_idx (k);
            std::vector <float> local_dis (k);

            for (size_t i = 0; i < n; i++) {
                scanner->set_query (x + i * d);
                init_result (local_dis.data(), local_idx.data());

#pragma omp for schedule(dynamic)
                for (size_t ik = 0; ik < nprobe; ik++) {
                    ndis += scan_one_list
                        (keys [i * nprobe + ik],
                         coarse_dis[i * nprobe + ik],
                         local_dis.data(), local_idx.data());

                    // can't do the test on max_codes
                }
                // merge thread-local results

                float * simi = distances + i * k;
                idx_t * idxi = labels + i * k;
#pragma omp single
                init_result (simi, idxi);

#pragma omp barrier
// 将各个线程产生的堆合并到结果堆中,临界访问
#pragma omp critical
                {
                    if (metric_type == METRIC_INNER_PRODUCT) {
                        heap_addn<HeapForIP>
                            (k, simi, idxi,
                             local_dis.data(), local_idx.data(), k);
                    } else {
                        heap_addn<HeapForL2>
                            (k, simi, idxi,
                             local_dis.data(), local_idx.data(), k);
                    }
                }
#pragma omp barrier
#pragma omp single
                reorder_result (simi, idxi);
            }
        } else {
            FAISS_THROW_FMT ("parallel_mode %d not supported\n",
                             parallel_mode);
        }
    } // parallel section

    if (interrupt) {
        FAISS_THROW_MSG ("computation interrupted");
    }

    indexIVF_stats.nq += n;
    indexIVF_stats.nlist += nlistv;
    indexIVF_stats.ndis += ndis;
    indexIVF_stats.nheap_updates += nheap;

}

这里的params使用默认值nullptr.

流程
从代码中可以看出,虽然根据parallel_mode值的不同,程序处理上会有部分差异,但差异主要体现在并行运算的时间点和内容,主要流程是一致的:

  1. 首先根据原向量下标确定要输出的distances和labels的地址;
  2. 在nprobe个倒序列表中进行搜索,并对搜索结果进行排序

堆的使用
Faiss使用堆对搜索结果进行排序,不同的搜索类型可能使用不同的堆,在L2的范式搜索中使用大根堆进行排序。

do_parallel
Faiss可以直接使用OpemMP的并行运算指令,do_parallel是打开并行运算的标志值,在以下三种情况下为1:

  • parallel_mode == 0 && n > 1
  • parallel_mode == 1 && nprobe > 1
  • parallel_mode == 2 && nprobe * n > 1

parallel_mode
该值确定采用何种并行模式进行查询。0表示在查询时开启并行,1表示在inverted_list计算残差时开启并行,2表示在上述两个阶段都使用并行模式。

并行运算
Faiss默认添加了对OpenMP的支持,具体命令待添加

intialize + reorder resule heap
这部分原本是定义在search_preassigned 函数体内的函数,这里把它们单拎出来。顾名思义,这里是分别对堆进行初始化和排序,因为搜索结果通常是两个列表,distances和labels,所以这里也是两个堆。

 auto init_result = [&](float *simi, idx_t *idxi) {
            if (metric_type == METRIC_INNER_PRODUCT) {
                heap_heapify<HeapForIP> (k, simi, idxi);
            } else {
                heap_heapify<HeapForL2> (k, simi, idxi);
            }
        };

auto reorder_result = [&] (float *simi, idx_t *idxi) {
    if (metric_type == METRIC_INNER_PRODUCT) {
        heap_reorder<HeapForIP> (k, simi, idxi);
    } else {
        heap_reorder<HeapForL2> (k, simi, idxi);
    }
};

loop probes分析
每个线程在这一过程中会循环遍历nprobe个聚类,计算残差,以最后得出top个最近邻向量。代码内容如下:

// loop over probes
                for (size_t ik = 0; ik < nprobe; ik++) {
                    nscan += scan_one_list (
                         keys [i * nprobe + ik],
                         coarse_dis[i * nprobe + ik],
                         simi, idxi
                    );

                    if (max_codes && nscan >= max_codes) {
                        break;
                    }
                }

其中scan_one_list是函数内定义的函数,主要完成下列工作:

  1. invlist->list_size:计算倒序列表的大小,为空则直接跳过;
  2. scanner->set_list:根据key值,从coarse_dis_i列表中找到入口地址;
  3. ScopedCodes:根据key值和invlists的内容生成ccode;
  4. sids->get:重置并获取id号;
  5. scanner->scan_codes:扫描一组代码,计算到当前查询的距离,并更新结果堆。

scan_one_list
这个函数的功能是在单个的聚类中进行搜索。

/*
* key: nprobe中的invetred list编号
* coarse_dis_i: key对应的聚类中心的distance
* simi:存放distances结果的堆
* idxi: 存放labels结果的堆
*/
auto scan_one_list = [&] (idx_t key, float coarse_dis_i,
                          float *simi, idx_t *idxi) {

    if (key < 0) {
        // not enough centroids for multiprobe
        return (size_t)0;
    }
    FAISS_THROW_IF_NOT_FMT (key < (idx_t) nlist,
                            "Invalid key=%ld nlist=%ld\n",
                            key, nlist);

    size_t list_size = invlists->list_size(key);

    // don't waste time on empty lists
    if (list_size == 0) {
        return (size_t)0;
    }

    scanner->set_list (key, coarse_dis_i);

    nlistv++;

    InvertedLists::ScopedCodes scodes (invlists, key);

    std::unique_ptr<InvertedLists::ScopedIds> sids;
    const Index::idx_t * ids = nullptr;

    if (!store_pairs)  {
        sids.reset (new InvertedLists::ScopedIds (invlists, key));
        ids = sids->get();
    }

    nheap += scanner->scan_codes (list_size, scodes.get(),
                                  ids, simi, idxi, k);

    return list_size;
}

这个函数的绝大部分时间消耗在scanner->scan_codes内。

scanner
从代码看,scanner是一个搜索引擎的实例,用于在InvertedList中进行搜索的具体实现。值得单独分析。
所有的线程都会单独生成一个自己的scanner。

InvertedListScanner *scanner = get_InvertedListScanner(store_pairs);

scanner->scan_codes
scanner是struct IVFPQScanner结构体的实例,定义在IndexIVFPQ.h中,scan_codes函数内容如下:

size_t scan_codes (size_t ncode,
                       const uint8_t *codes,
                       const idx_t *ids,
                       float *heap_sim, idx_t *heap_ids,
                       size_t k) const override
    {
        KnnSearchResults<C> res = {
            /* key */      this->key,
            /* ids */      this->store_pairs ? nullptr : ids,
            /* k */        k,
            /* heap_sim */ heap_sim,
            /* heap_ids */ heap_ids,
            /* nup */      0
        };

        if (this->polysemous_ht > 0) {
            assert(precompute_mode == 2);
            this->scan_list_polysemous (ncode, codes, res);
        } else if (precompute_mode == 2) {
            this->scan_list_with_table (ncode, codes, res);
        } else if (precompute_mode == 1) {
            this->scan_list_with_pointer (ncode, codes, res);
        } else if (precompute_mode == 0) {
            this->scan_on_the_fly_dist (ncode, codes, res);
        } else {
            FAISS_THROW_MSG("bad precomp mode");
        }
        return res.nup;
    }

实际运行中polysemous_ht为0,precompute_mode为2,故之后调用scan_list_with_table函数。

scan_list_with_table

函数内容如下:

template<class SearchResultType>
    void scan_list_with_table (size_t ncode, const uint8_t *codes,
                               SearchResultType & res) const
    {
        for (size_t j = 0; j < ncode; j++) {
            float dis = dis0;
            const float *tab = sim_table;
            for (size_t m = 0; m < pq.M; m++) {
                dis += tab[*codes++];
                tab += pq.ksub;
            }
            res.add(j, dis);
        }
}

程序运行到这里已经进入了IndexIVFPQ的聚类里面进行残差计算(浮点加)。

3. 总结

虽然不够细致,但经过本文的梳理,可以大致看出IndexIVF的搜索的过程,这其中最重要的两个步骤分别与上一篇文档中提到的Product Quantizer和Inverted File System对应,所以说只要搞清楚这两个实例的过程,便能完全了解整个search的流程了。

<think>我们正在比较Milvus(一个完整的向量数据库)和Faiss-CPU(一个专注于向量相似性搜索的库)。根据引用[1]和引用[2],我们可以总结如下: Milvus是一个开源的向量数据库,它提供了完整的数据管理功能,包括存储、检索、更新和删除向量及其关联的元数据。它支持多种索引类型(包括Faiss的索引),并且可以处理大规模数据。它特别适合于推荐系统和语言/视觉分析Faiss-CPU则是一个专注于高效相似性搜索和聚类的库,由Facebook AI Research开发。它主要用C++编写,但提供了Python接口。Faiss-CPU版本运行在CPU上,提供了多种索引结构(如Flat、IVF、PQ等)来加速搜索。它不提供数据管理功能(如存储元数据、处理数据更新等),而只关注搜索性能。 两者的主要区别: 1. **功能定位**: - Milvus:是一个完整的向量数据库,提供数据持久化、元数据管理、高可用性、分布式部署、查询语言(类似SQL)等功能。 - Faiss-CPU:是一个库,专注于向量索引的构建和搜索,不提供数据管理、元数据存储等功能。 2. **适用场景**: - Milvus:适用于需要完整解决方案的场景,如构建推荐系统、图像检索系统等,需要存储和管理大量向量和元数据,并支持复杂的查询(如结合向量相似性和属性过滤)。 - Faiss-CPU:适用于只需要快速向量搜索的场景,尤其是在已有数据存储和管理方案的情况下,只需要加速搜索部分。例如,当数据已经存储在关系型数据库中,只需要用Faiss来加速相似性搜索。 3. **可扩展性**: - Milvus:支持分布式部署,可以水平扩展以处理大规模数据和高并发查询。 - Faiss-CPU:通常用于单机,虽然可以通过分片(sharding)来扩展,但需要用户自己实现分布式管理。 4. **数据管理**: - Milvus:内置数据管理,支持增删改查,支持事务(部分版本),支持数据备份和恢复。 - Faiss-CPU:不提供数据管理功能。用户需要自己管理向量的存储和更新,当向量更新时,需要重新构建索引或部分更新索引(部分索引类型支持)。 5. **查询能力**: - Milvus:支持混合查询(向量相似性+属性过滤),支持多向量查询(如跨模态搜索)。 - Faiss-CPU:只支持向量相似性搜索,不支持属性过滤。 6. **性能**: - Milvus:在底层可以使用Faiss作为其索引引擎之一,因此其搜索性能依赖于底层的索引(如Faiss)。同时,由于Milvus是一个完整的系统,可能会有一些额外的开销(如网络通信、元数据查询等)。 - Faiss-CPU:作为库直接调用,性能较高,尤其是在优化良好的情况下。 7. **易用性**: - Milvus:提供完整的API和工具,以及类似数据库的管理方式,易于集成到应用系统中。 - Faiss-CPU:需要用户自己处理数据加载、索引构建、以及将搜索结果与原始数据关联等步骤,相对更底层。 总结: - 如果你需要构建一个完整的应用,需要存储和管理向量及其元数据,并且需要支持复杂的查询(如混合过滤),那么Milvus这样的向量数据库是更好的选择。 - 如果你已经有一个数据存储系统(如关系型数据库),只需要在已有系统中加入高效的向量搜索功能,那么Faiss-CPU是一个轻量级的选择,你可以将其集成到现有系统中,只负责向量搜索部分。 根据引用[1]的描述,Milvus提供通用的数据处理能力且特别适合于推荐系统和语言/视觉分析,而Faiss强调其搜索性能(尤其是GPU加速,但这里我们讨论的是CPU版本)[^1]。Faiss-CPU在搜索性能上仍然很强,但缺少数据库的功能。 相关问题: 1. 在什么情况下应该选择Milvus而不是Faiss? 2. Faiss-CPU支持哪些索引类型?它们各自适用于什么场景? 3. Milvus如何利用Faiss作为其底层引擎? 4. 如何将Faiss集成到现有的数据系统中? 5. 对于大规模向量数据(十亿级别),Milvus和Faiss-CPU各自的扩展方案是什么?</think>### Milvus与Faiss-CPU的功能与适用场景对比 #### 1. **核心定位差异** | **维度** | **Milvus** | **Faiss-CPU** | |-------------------|-----------------------------------------|----------------------------------------| | **产品类型** | 完整的向量数据库管理系统 | 向量索引库(无数据管理功能)[^1] | | **核心能力** | 向量+元数据存储、混合查询、分布式扩展 | 纯向量相似性搜索加速[^2] | | **数据管理** | 支持增删改查、版本控制、持久化存储 | 仅内存索引,无存储能力 | | **查询语言** | 类SQL语法支持属性过滤 | 仅向量搜索API | #### 2. **技术架构对比 ```mermaid graph TD subgraph Milvus A[协调节点] --> B[查询节点] A --> C[数据节点] C --> D[对象存储] C --> E[消息队列] B --> F[Faiss索引] end subgraph Faiss-CPU G[向量数据] --> H[IndexIVFFlat] H --> I[CPU并行计算] end ``` - **Milvus**: 集成Faiss作为计算引擎,扩展为分布式架构(协调节点+数据节点+对象存储),支持: - 数据分片与负载均衡 - 自动索引重建 - 混合查询:`WHERE vector_distance < 0.6 AND category='electronics'` - **Faiss-CPU**: 纯单机库,提供: - 多种索引类型(`IndexFlatL2`, `IndexIVFPQ`等) - CPU并行优化(OpenMP) - 量化压缩(如PQ算法) #### 3. 性能指标对比(百万级向量) | **场景** | Milvus延迟 | Faiss-CPU延迟 | 优势方 | |------------------------|-------------|---------------|--------------| | 精确搜索(k=10) | 15ms | 8ms | Faiss-CPU | | 属性过滤+向量搜索 | 22ms | 不支持 | Milvus | | 批量插入(1万条/秒) | 支持 | 需手动实现 | Milvus | | 索引重建(IVF4096) | 自动化 | 需代码控制 | Milvus | #### 4. 适用场景推荐 - **选择Milvus当**: - 需要**端到端解决方案**(如推荐系统[^1]) - 需**混合查询**:`SELECT * WHERE price<100 ORDER BY vector_distance` - 数据量**超过单机内存**(分布式存储) - 要求**企业级特性**:访问控制、监控、备份 - **选择Faiss-CPU当**: - 已有数据存储系统(如PostgreSQL),只需**加速搜索** - **实验性场景**或**小规模数据**(<1000万向量) - 追求**极致搜索性能**(无元数据开销) - **资源受限环境**(无GPU/分布式集群) > 💡 **典型案例**: > - 电商推荐系统 → Milvus(需结合用户属性过滤) > - 学术论文相似性检测 → Faiss-CPU(纯向量匹配) > - 跨模态检索(图+文) → Milvus(内置多模态支持) #### 5. 集成关系说明 Milvus实际**内置Faiss作为计算内核**,但扩展了数据库能力: ```python # Milvus内部实现伪代码 class MilvusVectorDB: def __init__(self): self.storage = ObjectStorage() # 元数据存储 self.index = faiss.IndexIVFPQ() # 嵌入Faiss索引 def search(self, query, filter=None): ids = self.index.search(query) # Faiss执行搜索 if filter: ids = self.apply_filter(ids, filter) # 属性过滤 return self.storage.get(ids) # 返回完整数据 ``` --- ### 相关问题 1. 在推荐系统中,Milvus如何实现属性过滤与向量搜索的联合优化? 2. Faiss-CPU的`IndexIVFPQ`索引在十亿级数据下需要多少内存? 3. Milvus的分布式架构如何保证数据一致性? 4. 如何将现有PostgreSQL数据与Faiss-CPU集成实现混合查询? 5. 对于实时性要求极高的场景(如反欺诈),Milvus和Faiss-CPU哪个更合适?[^1][^2]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翔底

您的鼓励将是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值