Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
chasingegg committed Jun 30, 2024
1 parent 736b942 commit a8d46e8
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 0 deletions.
104 changes: 104 additions & 0 deletions content/posts/2023-11-12-spfresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
author: "Chao.G"
title: "SPFresh: Incremental In-Place Update for Billion-Scale Vector Search"
date: "2023-11-12"
markup: "mmark"
draft: false
tags: ["ANN"]
categories: ["Papers"]
---

今天介绍一篇发在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,当时就预告了这篇工作已经中了,最近论文已经放出来了,于是就拿来看看。

## 背景

更新一直是向量索引里的老大难问题,文章实现了一个SPFresh系统,里面核心算法叫LIRE(lightweight incremental rebalancing protocol),可以高效地处理向量索引的更新问题。为什么向量索引的更新如此之难,拿目前最为流行的两类索引即图索引和IVF类索引举例,图索引处理向量插入需要在索引中找到距离新插入点的近邻进行双向连边,并且由于图索引的出度一般有上限,可能会触发裁边操作,导致插入开销略有些大。但更困难的是删除,首先要完全地在图中移除点就是一个很耗时的操作,因为一般索引里面不会维护点的入边,其次移除点可能是一个所谓的"hub"节点,可能会导致图索引的联通性遭到破坏,所以一般只会对删除点做标记删除,但这样仍然会对图索引的查询性能造成很大影响。IVF类索引就会比图索引在这个问题要好处理一些,新插入点只需要找到距离自己最近的聚类直接append即可,删除点就直接移除,但是持续更新以后,可能原来索引的聚类中心已经完全不能代表这个聚类了,或者可能聚类大小非常不平衡,所以也会导致查询性能的下降。

下图就是SPANN(IVF类索引)在静态数据上和动态数据上的性能差异,还是有比较明显的差距的,经过一段时间的更新以后,索引性能下降。

![spfresh](/assets/spfresh-static-in-place.png)

那么一般现在向量数据库系统采取的方式就是周期性地重建索引。作者这里列举了一些数字说明在大数据量下重建一个DiskANN/SPANN索引的开销,可能是需要以天计的。而SPFresh希望在保持索引性能的前提下,提供不错的索引更新能力。

## SPANN recap

这篇工作可以算是[SPANN](https://jingdongwang2017.github.io/Pubs/NeurIPS2021-SPANN.pdf)的后续工作,首先简单回顾下SPANN的做法,如下图所示,它会通过kmeans将数据划分成非常多的聚类,平均每个聚类的大小可能在10个数据点左右,每个聚类的大小是相对均匀的,另外通过对一些聚类边缘的数据点做replica,保证这些点更容易被搜到从而提升recall。

![spann](/assets/spfresh-spann.png)


## LIRE算法

LIRE算法上其实是非常直观的,

首先定义一个NPA(nearest partition assignment)性质:即每个向量都应该放置在距离它最近的partition中,这个partition指代一个聚类。如下图所示中聚类A,B,中间更新操作发生时,可能会违反NPA。

![spfresh](/assets/spfresh-npa.png)

### Insert & Delete

Insert和Delete是外部接口,由用户触发,接口完成以后很快返回,然后这些更新操作可能会触发以下一些系统内部的行为。

### Split & Merge

当一个聚类大小超过一定阈值以后就需要做split操作,把它分割成两个小的聚类,但这时可能就会违反NPA,这时候会触发reassign操作。如图中A分割成A1和A2以后,黄色的点可能距离聚类B更近。

当聚类大小因为删除点导致变小,小于一定阈值后就会和其他的聚类做合并,此时如果违反了NPA,同样会进行reassgin操作。如图中绿色的点可能距离新产生的聚类A2更近。

### Reassign

对于merge触发的reassign比较简单,老聚类的点append过去以后,某些点发现新的聚类并不满足NPA,重新选择合适的聚类。

而split操作会复杂一些,因为它会产生新的聚类。split时,老聚类$$A_0$$中的向量v,满足下面条件需要可能会被reassign,$$D(v, A_0) \leq D(v, A_i), \forall i \in 1, 2$$,这很好理解,说明原来聚类中心和v很接近,而换到新的聚类以后,v和聚类中心变远了,那么有可能v换到了一个"不那么好"的聚类,所以可能需要reassign。

邻近聚类中的某个点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到的位置。

另外,可能split会触发多次reassign,进而可能触发更多的split,导致产生级联split-reassign操作。作者给出了一个简单的证明表示这个行为可以在有限的次数收敛,因为每次split其实会增加一个聚类,而聚类不可能无限制地增加下去,它一定少于向量点的个数,所以这个级联操作一定是会在有限步数收敛的。


## SPFresh架构设计

![spfresh](/assets/spfresh-arch.png)

### Updater

Updater可以认为是一个和用户调用的insert/delete接口是对应的系统角色,插入时把新的向量append到聚类的最后,可能触发的split任务交给Local rebuilder。删除时通过修改内存中version map维护的删除标志位,真实数据的删除同样交给Local rebuilder。

### Local rebuilder

Local rebuilder是LIRE算法的核心,它会维护一个任务队列接收任务,然后执行具体的split/merge/reassign任务。需要注意的是在reassign某个向量时,Local rebuilder同样会修改version map中的版本号,并且把最新的版本号更新到storage中,此时这个点如果有一些replica就都成了过时数据,后续会被移除。这里我个人理解是作者应该是认为replica这些点是因为这些点处在聚类的边缘,所以在其他聚类中多复制一份增加recall,但是reassign以后可以认为这个点应该是处在自己合适的位置上,于是那些replica也就不需要了。

### Block controller

文章对磁盘上的数据布局进行了一定的设计,以及实现了一些简单的对磁盘数据的读写接口,这里还提到一个库[SPDK](https://spdk.io)直接操作SSD。

在数据布局上每个聚类上的点以<vector id, version number, raw vector>的形式存储,内存中会保存一份block mapping,可以直接从聚类id得到它包含的点的磁盘block offset。读操作发生时异步IO请求发送给IO request queue,SPDK库会进行高效的IO操作。在写新数据时,只会读取该聚类包含的数据的最后一个block,写入新的数据作为一个新的block,写入完成以后同时更新内存中的block mapping。


## 实验

文章进行了非常详实的实验佐证效果,这里简单提一下实验上的设计,这类系统的实验其实很难做到很公平,只能尽量做到标准统一。文章比较了DiskANN,加上naive update算法的SPANN(SPANN+)以及SPFresh。

模拟每天大约1%的数据删除和插入,由于各类算法的不同,把更新操作需要满足的QPS作为硬门槛,针对不同索引算法对insert/delete以及后台做split/merge/reassign等操作设置不同的线程数(DiskANN由于更新操作比较重,需要更多线程数),然后提供相同的线程数来做查询,比较查询的QPS以及recall,内存占用等,最后结果如下。

![spfresh-exp](/assets/spfresh-exp.png)

## 总结和思考

文章基于SPANN索引增强了其更新的能力,算法上思路是很简单的,在整个系统的工程实现上还是需要很大工作量去做,已经非常接近一个真实可用的系统。好像据说这篇工作已经在微软的bing中有被使用。

但是这里个人感觉很多时候重建的成本并不算太大。一般对于向量数据库系统而言,索引往往是基于数据分片而不是全量构建的,不同的数据分片可以在不同的node上充分利用到分布式的能力。所以在这些系统上,往往重建的开销可能没有那么大,相当于粒度更小,可能缓慢地重建索引让系统能力缓慢提升。比如下图所示。

![compaction](/assets/spfresh-milvus-compaction.png)

在很早的时候,看过浙大傅博士的一篇[知乎文章](https://zhuanlan.zhihu.com/p/100716181),个人认为很有道理。向量系统的持续更新可能是一个伪命题,因为向量本身并不是所谓的"source of truth",向量背后所代表的非结构化数据才是真实数据,向量只是在当前数据规模下刻画这些数据的状态,非常不稳定,模型更新以后就需要重新导入数据。另一类情况是批量删除,SPFresh主要还是面向流式删除(并且是少量)的场景,对于批量删除的场景可能并不如重建索引来得有效。

但这个工作我想仍然是有价值的,对于少量更新的场景(毕竟微软自己已经在用了嘛),在一段较短的时间内(可能几天?)可以使用SPFresh来让系统维持一个不错的性能。长期来看也许重建索引仍然是一个必然选择。


## Reference

- https://www.microsoft.com/en-us/research/publication/spfresh-incremental-in-place-update-for-billion-scale-vector-search/
- https://jingdongwang2017.github.io/Pubs/NeurIPS2021-SPANN.pdf
- https://spdk.io
- https://zhuanlan.zhihu.com/p/100716181
27 changes: 27 additions & 0 deletions content/posts/2024-01-01-farewell-2023.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
author: "Chao.G"
title: "再见2023"
date: "2024-01-01"
markup: "mmark"
draft: false
tags: ["Life"]
categories: ["Life"]
---

2023年就这么又过去了,很快。

常常感叹时间过得真快,一年又一年,年龄如同杂草一般疯长,渐渐地习惯以大叔,中年人这类词自居。人年龄上去以后,开始常常陷入回忆之中,而我的对策不是说去减少回忆,多向前期待,而是想着可以多记录一些,多留下一些文字,语音和影像,在未来更老的时候看看年轻时候的自己。

又是打工的一年,说来两年多的工作时间,但已经待过两个公司,四个组。。平均一个组就能待上半年,而在现在的组已经是待的时间最久的了,已经有一年多的时间。时间不长有个很大的坏处是做的事情不延续,很多东西是要靠时间积累的,可能一年是至少的吧,半年可能真的只是熟络了一下完全没有什么东西做出来,但是对于一些工作内容又让人确实不想干下去。。从另一个角度讲,在一个变动很快的职业之中,也许就可以快速做出决定,还有很多的职业并不具备这样的自由度,再叠加越来越难的就业环境,这已经是一种幸运了。

去年的时候有一些对工作不抱期待的不满论调,但事实上人就容易对这件事本能地投入精力,会比去年刚来新公司的时候会卷一些,当然比起字节还是好了太多。今年有了一种新的感觉,是说在工作上投入了太多精力,心里有一种愧疚感,觉得不值得,我觉得这倒是一件好事,说明逃脱了某种学生思维,之前上学的时候容易会因为自己没努力学习而愧疚,现在居然都开始反向愧疚了。对于工作的内容,我大体上还是比较认可的,大部分时候还是独立去推进一些事情做完,做了一些自己想做的项目,也做了一些更像是安排的但是也挺想尝试的项目,也有一些强插上来的项目。正好年初开始独立开展项目,一开始还是有些紧张刺激的,好在最后做的也还行。整体上半年,工作上更加轻松一些,更加确定,可以比较按照自己的节奏来,后面就开始变得很忙,会有更多中途的事情插进来,长期项目和短期项目的共同推进也是一个我正在学习中的事情,当更紧急的事情来了以后,如何让那个长期项目同样可以有效地推进下去。事情很多以后,也会有一些焦虑,而且经常有的一个状况是做着一件事情,脑子里在想另一件事,而且可能会飘很久,很像是上学时候的神游。

AI/LLM这些词显然是贯穿了整个2023年(尤其是上半年)的技术圈子,一度让我也是有点热血沸腾的意思,觉得自己站在了风口之中。可惜对于AI的理解还是约等于0,只能是说chatgpt确实很好用。本来也有想过去系统学习下AI知识,但最后还是没有,也是懒的可以。。对于AI不求更多,只求有个松散的认知即可。对于向量数据库这个有点小火的品种,当然在我还靠它吃饭的时候,还是希望它真的可以很火,虽然它本身并没有太多变化,但可能应用场景确实变多了。一个比较好的地方是毕竟这是一个AI和数据库都有关的领域,可以趁机有更多机会关注到AI的东西,但是重AI的人和重数据库的人显然是不一样的,重数据库的人对于这种玩具一样的系统可能觉得不堪一击,注重更多数据库应该需要提供的能力,重AI的人会聊更多跟AI去结合的场景,需求迭代更快。从我角度来看,我可能更倾向于AI这波人的方式,毕竟这个东西为AI服务的,它可以叫向量数据库,也可以不叫这个名字。但很可惜我还是更希望在数据库这个方向做更多事情,对于向量数据库后面能做的事情还是保持一个谨慎甚至略带消极的态度。这一年的感觉就是经常什么火火火,今天一个文章,明天又是一个,有时候有一种焦虑感,觉得自己根本跟不上,也会耗费时间想多看看跟人插上话,但是这个心态还是有点问题,还是应该慢下来,看点真正想看的东西。

因为开始参与公司的项目开发,今年的开源贡献肯定是史上最多的一年,但可惜没有参与到更多开源项目中去,希望新的一年可以参与更多的项目,最好能学学rust吧。写了一些技术文章,收获到了很小的关注,但还是很高兴,很多是一些博士生主动来交流想法,后面还是一定要继续多写写。

生活上感觉在现在租的房子里住得还是很舒服的,小是小了点,但是很方便也很温馨。今年算是去了挺多地方的,按照时间顺序是周庄,北京,扬州,高邮,桐庐,新疆,台州,逐渐开启了一种江浙沪周边躺平游的新模式,请个假去周边小县城感受一些安静的氛围,轻度逛逛+吃吃吃,回来在家再躺一天,这种小县城的感觉都还是不太一样的,很开心见到了这些有趣的东西。比起去年就去了个长沙要丰富多了,但是外面人也是真的多,可惜的是今年没能去成音乐节,新的一年还是想去。有了相机以后,还是多出去拍点东西,新年去更多地方吧。

作为高度社恐者,我比较自信我社交(不论是线上还是线下)的匮乏程度应该还是远超他人的,维护一些社交关系让我非常吃力,但是在有些时候又会对此有希望。能做的就是现在的好朋友保持一个好的关系,有空见见。而且心中有个不切实际的幻想,是也许我会和更外部空间的东西产生联系,所以我还是需要保持一些自我表达的能力。

2023感觉还行,可以说确实没有2022年那么失败,新年继续不失败。
Loading

0 comments on commit a8d46e8

Please sign in to comment.