|
| 1 | +--- |
| 2 | +author: "Chao.G" |
| 3 | +title: "SPFresh: Incremental In-Place Update for Billion-Scale Vector Search" |
| 4 | +date: "2023-11-12" |
| 5 | +markup: "mmark" |
| 6 | +draft: false |
| 7 | +tags: ["ANN"] |
| 8 | +categories: ["Papers"] |
| 9 | +--- |
| 10 | + |
| 11 | +今天介绍一篇发在SOSP2023的文章,[SPFresh: Incremental In-Place Update for Billion-Scale Vector Search](https://www.microsoft.com/en-us/research/publication/spfresh-incremental-in-place-update-for-billion-scale-vector-search/),讲的是向量检索系统的更新问题。这也是微软亚研院和中科大的工作,有幸前段时间听过微软陈琪老师的talk,当时就预告了这篇工作已经中了,最近论文已经放出来了,于是就拿来看看。 |
| 12 | + |
| 13 | +## 背景 |
| 14 | + |
| 15 | +更新一直是向量索引里的老大难问题,文章实现了一个SPFresh系统,里面核心算法叫LIRE(lightweight incremental rebalancing protocol),可以高效地处理向量索引的更新问题。为什么向量索引的更新如此之难,拿目前最为流行的两类索引即图索引和IVF类索引举例,图索引处理向量插入需要在索引中找到距离新插入点的近邻进行双向连边,并且由于图索引的出度一般有上限,可能会触发裁边操作,导致插入开销略有些大。但更困难的是删除,首先要完全地在图中移除点就是一个很耗时的操作,因为一般索引里面不会维护点的入边,其次移除点可能是一个所谓的"hub"节点,可能会导致图索引的联通性遭到破坏,所以一般只会对删除点做标记删除,但这样仍然会对图索引的查询性能造成很大影响。IVF类索引就会比图索引在这个问题要好处理一些,新插入点只需要找到距离自己最近的聚类直接append即可,删除点就直接移除,但是持续更新以后,可能原来索引的聚类中心已经完全不能代表这个聚类了,或者可能聚类大小非常不平衡,所以也会导致查询性能的下降。 |
| 16 | + |
| 17 | +下图就是SPANN(IVF类索引)在静态数据上和动态数据上的性能差异,还是有比较明显的差距的,经过一段时间的更新以后,索引性能下降。 |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +那么一般现在向量数据库系统采取的方式就是周期性地重建索引。作者这里列举了一些数字说明在大数据量下重建一个DiskANN/SPANN索引的开销,可能是需要以天计的。而SPFresh希望在保持索引性能的前提下,提供不错的索引更新能力。 |
| 22 | + |
| 23 | +## SPANN recap |
| 24 | + |
| 25 | +这篇工作可以算是[SPANN](https://jingdongwang2017.github.io/Pubs/NeurIPS2021-SPANN.pdf)的后续工作,首先简单回顾下SPANN的做法,如下图所示,它会通过kmeans将数据划分成非常多的聚类,平均每个聚类的大小可能在10个数据点左右,每个聚类的大小是相对均匀的,另外通过对一些聚类边缘的数据点做replica,保证这些点更容易被搜到从而提升recall。 |
| 26 | + |
| 27 | + |
| 28 | + |
| 29 | + |
| 30 | +## LIRE算法 |
| 31 | + |
| 32 | +LIRE算法上其实是非常直观的, |
| 33 | + |
| 34 | +首先定义一个NPA(nearest partition assignment)性质:即每个向量都应该放置在距离它最近的partition中,这个partition指代一个聚类。如下图所示中聚类A,B,中间更新操作发生时,可能会违反NPA。 |
| 35 | + |
| 36 | + |
| 37 | + |
| 38 | +### Insert & Delete |
| 39 | + |
| 40 | +Insert和Delete是外部接口,由用户触发,接口完成以后很快返回,然后这些更新操作可能会触发以下一些系统内部的行为。 |
| 41 | + |
| 42 | +### Split & Merge |
| 43 | + |
| 44 | +当一个聚类大小超过一定阈值以后就需要做split操作,把它分割成两个小的聚类,但这时可能就会违反NPA,这时候会触发reassign操作。如图中A分割成A1和A2以后,黄色的点可能距离聚类B更近。 |
| 45 | + |
| 46 | +当聚类大小因为删除点导致变小,小于一定阈值后就会和其他的聚类做合并,此时如果违反了NPA,同样会进行reassgin操作。如图中绿色的点可能距离新产生的聚类A2更近。 |
| 47 | + |
| 48 | +### Reassign |
| 49 | + |
| 50 | +对于merge触发的reassign比较简单,老聚类的点append过去以后,某些点发现新的聚类并不满足NPA,重新选择合适的聚类。 |
| 51 | + |
| 52 | +而split操作会复杂一些,因为它会产生新的聚类。split时,老聚类$$A_0$$中的向量v,满足下面条件需要可能会被reassign,$$D(v, A_0) \leq D(v, A_i), \forall i \in 1, 2$$,这很好理解,说明原来聚类中心和v很接近,而换到新的聚类以后,v和聚类中心变远了,那么有可能v换到了一个"不那么好"的聚类,所以可能需要reassign。 |
| 53 | + |
| 54 | +邻近聚类中的某个点v满足下列条件可能会被reassign,$$D(v, A_i) \leq D(v, A_0), \exists i \in 1, 2$$,因为v和新聚类的距离变近了,那么有可能比v和v所在的聚类B的距离更近。另外为了减小开销,LIRE只会检查split发生的聚类邻近的少量几个聚类做ressign检查,得到可能需要reassign的候选向量集合以后,遍历所有的聚类找到最近的作为新的需要被assign到的位置。 |
| 55 | + |
| 56 | +另外,可能split会触发多次reassign,进而可能触发更多的split,导致产生级联split-reassign操作。作者给出了一个简单的证明表示这个行为可以在有限的次数收敛,因为每次split其实会增加一个聚类,而聚类不可能无限制地增加下去,它一定少于向量点的个数,所以这个级联操作一定是会在有限步数收敛的。 |
| 57 | + |
| 58 | + |
| 59 | +## SPFresh架构设计 |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +### Updater |
| 64 | + |
| 65 | +Updater可以认为是一个和用户调用的insert/delete接口是对应的系统角色,插入时把新的向量append到聚类的最后,可能触发的split任务交给Local rebuilder。删除时通过修改内存中version map维护的删除标志位,真实数据的删除同样交给Local rebuilder。 |
| 66 | + |
| 67 | +### Local rebuilder |
| 68 | + |
| 69 | +Local rebuilder是LIRE算法的核心,它会维护一个任务队列接收任务,然后执行具体的split/merge/reassign任务。需要注意的是在reassign某个向量时,Local rebuilder同样会修改version map中的版本号,并且把最新的版本号更新到storage中,此时这个点如果有一些replica就都成了过时数据,后续会被移除。这里我个人理解是作者应该是认为replica这些点是因为这些点处在聚类的边缘,所以在其他聚类中多复制一份增加recall,但是reassign以后可以认为这个点应该是处在自己合适的位置上,于是那些replica也就不需要了。 |
| 70 | + |
| 71 | +### Block controller |
| 72 | + |
| 73 | +文章对磁盘上的数据布局进行了一定的设计,以及实现了一些简单的对磁盘数据的读写接口,这里还提到一个库[SPDK](https://spdk.io)直接操作SSD。 |
| 74 | + |
| 75 | +在数据布局上每个聚类上的点以<vector id, version number, raw vector>的形式存储,内存中会保存一份block mapping,可以直接从聚类id得到它包含的点的磁盘block offset。读操作发生时异步IO请求发送给IO request queue,SPDK库会进行高效的IO操作。在写新数据时,只会读取该聚类包含的数据的最后一个block,写入新的数据作为一个新的block,写入完成以后同时更新内存中的block mapping。 |
| 76 | + |
| 77 | + |
| 78 | +## 实验 |
| 79 | + |
| 80 | +文章进行了非常详实的实验佐证效果,这里简单提一下实验上的设计,这类系统的实验其实很难做到很公平,只能尽量做到标准统一。文章比较了DiskANN,加上naive update算法的SPANN(SPANN+)以及SPFresh。 |
| 81 | + |
| 82 | +模拟每天大约1%的数据删除和插入,由于各类算法的不同,把更新操作需要满足的QPS作为硬门槛,针对不同索引算法对insert/delete以及后台做split/merge/reassign等操作设置不同的线程数(DiskANN由于更新操作比较重,需要更多线程数),然后提供相同的线程数来做查询,比较查询的QPS以及recall,内存占用等,最后结果如下。 |
| 83 | + |
| 84 | + |
| 85 | + |
| 86 | +## 总结和思考 |
| 87 | + |
| 88 | +文章基于SPANN索引增强了其更新的能力,算法上思路是很简单的,在整个系统的工程实现上还是需要很大工作量去做,已经非常接近一个真实可用的系统。好像据说这篇工作已经在微软的bing中有被使用。 |
| 89 | + |
| 90 | +但是这里个人感觉很多时候重建的成本并不算太大。一般对于向量数据库系统而言,索引往往是基于数据分片而不是全量构建的,不同的数据分片可以在不同的node上充分利用到分布式的能力。所以在这些系统上,往往重建的开销可能没有那么大,相当于粒度更小,可能缓慢地重建索引让系统能力缓慢提升。比如下图所示。 |
| 91 | + |
| 92 | + |
| 93 | + |
| 94 | +在很早的时候,看过浙大傅博士的一篇[知乎文章](https://zhuanlan.zhihu.com/p/100716181),个人认为很有道理。向量系统的持续更新可能是一个伪命题,因为向量本身并不是所谓的"source of truth",向量背后所代表的非结构化数据才是真实数据,向量只是在当前数据规模下刻画这些数据的状态,非常不稳定,模型更新以后就需要重新导入数据。另一类情况是批量删除,SPFresh主要还是面向流式删除(并且是少量)的场景,对于批量删除的场景可能并不如重建索引来得有效。 |
| 95 | + |
| 96 | +但这个工作我想仍然是有价值的,对于少量更新的场景(毕竟微软自己已经在用了嘛),在一段较短的时间内(可能几天?)可以使用SPFresh来让系统维持一个不错的性能。长期来看也许重建索引仍然是一个必然选择。 |
| 97 | + |
| 98 | + |
| 99 | +## Reference |
| 100 | + |
| 101 | +- https://www.microsoft.com/en-us/research/publication/spfresh-incremental-in-place-update-for-billion-scale-vector-search/ |
| 102 | +- https://jingdongwang2017.github.io/Pubs/NeurIPS2021-SPANN.pdf |
| 103 | +- https://spdk.io |
| 104 | +- https://zhuanlan.zhihu.com/p/100716181 |
0 commit comments