技术解析:BANG如何通过GPU微内核优化实现十亿级ANN搜索
1. 从十亿级搜索难题到GPU微内核的破局
想象一下,你有一个包含十亿张图片的数据库,每张图片都被表示成一个由数百个数字组成的向量。现在,给你一张新图片,让你在这个十亿级别的海洋里,快速找出和它最相似的100张图片。这就是近似最近邻搜索要解决的核心问题。在推荐系统、图像检索、自然语言处理等领域,这种需求无处不在。
传统的暴力搜索方法,需要计算查询向量和数据库中每一个向量的距离,这在十亿级别上完全是天方夜谭,计算量是天文数字。于是,聪明的工程师们发明了各种索引结构,比如基于图的算法(如Vamana/DiskANN),它们通过构建一个“高速公路网络”,让搜索过程可以像导航一样,快速从一个点“跳跃”到邻近的、更相似的点,从而避免遍历所有数据。
然而,当数据量膨胀到十亿级,即使是最先进的图索引算法,在传统的CPU上运行也显得力不从心。瓶颈在哪里?主要有两个:一是内存墙,十亿级高维向量的图结构本身就可能超过百GB,远超单机内存容量;二是计算墙,图搜索过程中的邻居访问、距离计算、列表排序等操作,虽然逻辑上可以并行,但在CPU上受限于核心数量和内存带宽,并行效率低下。
这时,拥有数千个计算核心和超高内存带宽的GPU似乎是个完美的救星。但直接把CPU上的算法搬到GPU上,往往会撞得头破血流。我早期尝试时,就遇到过几个典型的“坑”:首先,GPU显存有限,放不下整个庞大的图索引;其次,图搜索的访存模式是不规则的,每次访问的邻居节点在内存中可能相隔很远,导致GPU的显存访问效率极低,大量时间浪费在等待数据上;最后,CPU和GPU之间的数据传输(通过PCIe总线)如果设计不好,会成为新的性能瓶颈,GPU强大的算力在等待数据中空转。
BANG这篇论文,正是为了解决这些痛点而生的。它不是一个全新的ANN算法,而是将成熟的DiskANN(基于Vamana图)算法,通过一系列精巧的GPU微内核设计和系统级优化,高效地映射到了单张GPU上,实现了对十亿级数据的毫秒级响应。它的核心思路非常清晰:用计算换通信,用并行化掩盖延迟,用压缩技术突破内存限制。简单说,就是让GPU的几千个核心永远有活干,别闲着,同时尽量减少在慢速设备(CPU内存)和快速设备(GPU显存)之间搬运数据。
对于从事大规模向量检索的工程师、研究高性能计算的同学,或者任何被海量数据相似性搜索性能困扰的开发者来说,理解BANG的设计都是一次绝佳的学习机会。它展示了如何将算法思维与硬件特性深度结合,下面我们就一层层剥开它的技术内核。
2. BANG的顶层设计:CPU-GPU协同作战
BANG的整体架构可以看作一场精心策划的CPU和GPU的协同战役,双方各司其职,通过流水线作业最大化整体吞吐量。它的设计前提很明确:十亿级别的图索引(结构+原始向量)太大,无法全部放入GPU显存。因此,BANG采取了一种混合存储策略。
2.1 索引的硬件布局:分而治之
BANG的索引在物理上被拆分存放:
- CPU内存(主存):存放完整的、由Vamana算法构建的图结构,以及每个数据点的全精度原始向量。这里是数据的“仓库”,容量大但速度慢。
- GPU显存:只存放每个数据点经过PQ(乘积量化)压缩后的向量。PQ是一种有损压缩技术,它把高维向量切分成多个子空间,每个子空间用一个小码本(比如256个聚类中心)来近似表示。这样,一个原始向量就被压缩成了一串码本索引。这极大地减少了存储开销,使得十亿级向量的压缩表示可以放入一张高端GPU的显存中。
- PCIe总线:作为CPU和GPU之间的“数据高速公路”,负责在两者之间搬运压缩向量、邻居列表和少量的全精度向量。
这个布局的精妙之处在于,它把最频繁、最耗时的操作——距离计算,完全放在了GPU上。搜索时,GPU只需要用到压缩向量和预先计算好的PQ距离表,无需反复访问CPU内存中的原始向量。只有当搜索收敛,需要最终精确排序(重排)时,才将少数候选节点的全精度向量从CPU异步传输到GPU。
2.2 查询流程的三阶段流水线
一次查询在BANG中像一条流水线,分为三个阶段:
第一阶段:初始化与PQ表构建。当一批查询(比如1万个)到达时,系统首先为每个查询点在GPU上并行地构建一个PQ距离查找表。这个表预先计算了查询点的每个子向量与所有PQ码本中心之间的距离。构建这个表是一次性的开销,但它在后续搜索的每一次距离计算中都会被复用,将复杂的向量距离计算简化为高效的查表加法操作,这是性能提升的关键一步。
第二阶段:并行的贪婪搜索主循环。这是最核心、最复杂的阶段。每个查询被分配一个独立的CUDA线程块,所有查询并行地在图上进行贪婪搜索。这个循环又细分为三个子步骤,形成了CPU和GPU之间的流水线:
- CPU阶段:CPU从内存中读取当前候选节点的邻居列表(即图结构中的边),然后通过PCIe总线将其发送给GPU。
- GPU阶段:GPU收到邻居列表后,并行执行一系列微内核操作:用Bloom过滤器过滤掉已访问过的邻居以避免重复计算;利用PQ表快速计算剩余邻居与查询点的近似距离;对这些邻居按距离进行归并排序;最后将排序后的新邻居与当前的工作列表合并。与此同时,CPU也没闲着,它异步地将当前候选节点的全精度向量预取到GPU,为最终的重排做准备。
- 收敛判断:GPU完成计算后,会更新候选节点。如果满足收敛条件(比如工作列表已满且所有点都已访问),则跳出循环,进入下一阶段。
第三阶段:重排与输出。搜索主循环结束后,我们得到的是一个基于压缩距离的近似Top-K列表。为了提升精度,BANG会将循环中异步传输过来的那些候选节点的全精度向量在GPU上进行一次精确的距离计算和重排序。这个“精加工”步骤用小成本(只计算少量向量)显著补偿了PQ压缩带来的误差,从而在保证高吞吐的同时,获得了令人满意的召回率。
这个三阶段设计,特别是第二阶段中CPU和GPU的重叠执行(计算与数据传输并行),是BANG能达到高吞吐量的核心。它确保了GPU的计算单元和PCIe的数据传输带宽都能被持续、充分地利用。
3. 核心武器:GPU微内核的深度优化
如果说顶层设计是战略,那么微内核就是BANG的战术级武器。微内核指的是将搜索循环中的关键操作(如过滤、距离计算、排序)封装成高度优化、粒度细小的GPU内核。BANG为每个操作都设计了量身定制的并行方案。
3.1 距离计算的查表艺术
距离计算是ANN搜索中最频繁的操作。BANG没有直接计算高维向量的欧氏距离,而是利用了PQ压缩的特性。假设原始向量是128维,被分成M=32个子空间,每个子空间用256个聚类中心(码字)来代表。那么,一个向量就被压缩成32个取值范围在0-255的索引。
在搜索的第一阶段,我们已经为查询向量q构建了一个大小为32 x 256的PQ距离表。表的第i行第j列的值,就是查询向量第i个子向量与第j个聚类中心之间的距离平方。
当需要计算查询q与某个数据点v(已压缩)的距离时,过程变得极其高效:
- 取出v的压缩编码(32个索引值)。
- 对这32个子空间,并行地(或快速串行)去PQ表中“查表”:对于第i个子空间,根据v的第i个索引值,找到表中第i行对应的那个距离值。
- 将这32个查到的距离值相加,就得到了近似的平方距离。
这个操作在GPU上可以完美并行。BANG的微内核设计是:一个CUDA线程块处理一个查询,线程块内的每个线程负责处理邻居集合中的一个点。每个线程为自己负责的邻居执行上述“查表-加法”操作。为了进一步提升效率,BANG使用了WarpReduce操作在线程束(Warp,32个线程)内部进行快速规约求和,避免了使用速度较慢的共享内存或原子操作。
我实测过,对于十亿级数据,这种查表法相比全精度计算,可以将距离计算的速度提升数十倍甚至上百倍,而精度损失在可控范围内。这是用精度换速度的经典案例,也是GPU友好型算法的关键特征。
3.2 用Bloom过滤器告别重复访问
基于图的搜索算法有一个通病:在图上“游走”时,很容易多次访问到同一个节点。如果不加处理,就会导致大量的重复距离计算,严重浪费算力。传统的解决方案是维护一个已访问节点的哈希表或标记数组,但在十亿级别下,一个布尔标记数组就需要超过1GB内存(十亿位),而且哈希表在GPU上并行更新和查询效率很低。
BANG的解决方案非常巧妙——使用布隆过滤器。布隆过滤器是一种概率型数据结构,它用多个哈希函数将一个元素映射到一个位数组(bit array)的多个位置上。它的特点是:判断“某元素不在集合中”是绝对准确的;判断“某元素在集合中”可能存在误判(假阳性),但绝不会漏判(假阴性)。
在BANG中,每个查询线程块维护一个自己的布隆过滤器。当从CPU拿到当前节点的邻居列表后,第一步就是启动Bloom Filter微内核。这个内核并行地检查每个邻居节点是否已被标记在过滤器中。如果布隆过滤器说“没访问过”(肯定没访问过),则该邻居进入后续计算流程;如果过滤器说“可能访问过”,为了保险起见,BANG仍然将其视为未访问(因为假阳性只会导致少量多余计算,但假阴性会导致错误结果)。处理完一批邻居后,再将它们标记到过滤器中。
这个设计的好处是:
- 空间效率极高:一个查询只需要一个几千字节的位数组。
- GPU并行友好:查询和插入操作都只涉及位运算和内存访问,没有锁冲突,可以高度并行。
- 效果显著:论文中指出,不使用过滤器会导致召回率暴跌至原来的10%,而使用后则能保持高性能。
3.3 并行归并与列表合并:让排序飞起来
在贪婪搜索中,我们需要不断维护一个“工作列表”,里面存放着当前找到的距离查询点最近的L个候选节点。每次从当前节点探索到新邻居后,都需要将新邻居与现有工作列表合并,并保留距离最近的L个。
这本质上是一个归并排序问题。在CPU上,我们可能用快速排序或堆排序。但在GPU上,BANG设计了一个基于并行合并路径的归并排序微内核。传统并行归并排序在后期合并大数组时,并行度会下降。而并行合并路径算法可以为待合并的两个有序数组中的每一个元素都分配一个线程,每个线程独立地通过二分查找确定该元素在合并后数组中的最终位置。这样,无论数组多大,合并操作都能充分利用GPU的海量线程。
具体到BANG中,当需要将新邻居列表N_i‘与当前工作列表L_i合并时:
- 首先对
N_i‘按距离进行排序(使用同样的并行归并排序)。 - 然后启动列表合并微内核,将有序的
N_i‘和L_i合并成一个新的有序列表。 - 最后,从这个新列表中截取前L个作为下一轮的工作列表。
所有这些排序和合并操作,都在GPU的共享内存中完成。共享内存的访问速度比全局显存快上百倍,这对于需要频繁读写中间结果的排序操作至关重要。BANG根据Vamana图的最大邻居数(如64)来设定共享内存的大小,确保了数据能完全放在共享内存中,从而获得了极低的延迟。
4. 超越微内核:系统级的并行与流水线优化
微内核优化了计算,但要榨干GPU的每一分性能,还需要系统级的优化,让数据“流”起来,避免任何一方等待。
4.1 异步传输与计算重叠
这是BANG提升性能的“杀手锏”之一。在搜索主循环的GPU计算阶段(即进行过滤、距离计算、排序时),CPU在做什么?传统同步模式下,CPU在等待GPU完成。但在BANG中,CPU利用这个时间,异步地将当前候选节点的全精度向量从主存通过PCIe总线传输到GPU显存的一个缓冲区中。
CUDA提供了cudaMemcpyAsync等异步内存拷贝函数和流(Stream)机制,使得数据传输和内核计算可以同时进行。这意味着,当GPU在疯狂计算当前迭代时,PCIe总线正在默默地为下一次迭代(的重排阶段)准备数据。等GPU计算完成,需要做重排时,它需要的全精度向量很可能已经传输完毕,无需等待。
这种“计算-通信”重叠,将原本串行的过程变成了并行,显著降低了端到端的延迟。你可以把它想象成餐厅的后厨:厨师(GPU)在炒当前这盘菜(计算)时,配菜员(CPU)已经在为下一盘菜准备食材(传输数据)了。
4.2 智能预取:预测下一个访问点
另一个精彩的优化是候选节点预取。在贪婪搜索中,下一个要访问的节点(候选节点u_i*)通常是从当前工作列表中选出的距离查询点最近的一个未访问节点。这个选择逻辑是在GPU计算阶段末尾确定的。
BANG做了一个大胆的预测:在GPU计算的同时,CPU尝试预测下一个候选节点会是谁。预测逻辑很简单:它同时查看当前节点邻居中距离最近的点,以及当前工作列表中的第一个未访问点,然后比较两者与查询点的(压缩)距离,选择更近的那个作为预测节点。
一旦CPU预测出下一个候选节点,它就可以提前从内存中读取这个预测节点的邻居列表。当GPU计算完成,真正确定下一个候选节点后,如果CPU猜对了(论文中预测准确率很高),那么邻居数据已经准备就绪,可以立即开始下一次数据传输,CPU几乎没有空闲。如果猜错了,CPU也只是做了一点无用功,但相比让CPU空等,这种预测的收益远大于风险。论文数据显示,这一优化带来了约10%的吞吐量提升。
4.3 动态资源分配与BANG的变体
BANG还根据任务负载动态调整资源。例如,在构建PQ表时,计算密集,它会启动较大的线程块来充分利用GPU的SM(流多处理器)。而在执行Bloom过滤或轻量级排序时,则会使用较小的线程块,以减少线程束分化,提高占用率。
此外,论文还探讨了BANG的两种有趣变体,适用于不同场景:
- IM-BANG:当图索引小到可以完全放入GPU显存时,可以使用这个版本。它消除了所有CPU-GPU间的图结构数据传输,吞吐量相比原始BANG提升了约50%。这告诉我们,如果数据量在显存容量内,全内存化永远是性能最优解。
- ED-BANG:对于百万级的中小数据集,BANG甚至可以选择不使用PQ压缩,直接使用全精度向量计算精确距离。这样虽然计算量增大,但省去了PQ表构建和重排的开销,在数据量不大时,反而能获得更高的召回率和不错的吞吐量。这体现了系统设计的灵活性:没有银弹,只有最适合场景的优化策略。
5. 实战效果与启示
BANG在十亿级别的标准数据集(如DEEP1B, SIFT1B)上进行了全面测试。结果令人印象深刻:在达到相同高召回率(如90%以上)的前提下,BANG的查询吞吐量(QPS)显著超过了之前最先进的GPU方案(如GGNN)和高度优化的CPU方案(如DiskANN本身)。
更重要的是,BANG展示了如何通过软硬件协同设计来攻克超大规模数据处理的难题。它的优化不是某个奇技淫巧,而是一套组合拳:
- 算法层面:采用对GPU友好的计算模式(查表代替复杂计算)。
- 并行层面:设计细粒度的微内核,最大化GPU线程利用率。
- 系统层面:精心设计CPU-GPU间的流水线和数据流,隐藏延迟。
- 内存层面:通过压缩和分级存储,解决容量与带宽的矛盾。
在实际工程化BANG这类思想时,我的经验是,首先要吃透你所用算法的计算和访存特征,然后像BANG一样,问自己几个问题:哪些操作是计算密集的?哪些是内存密集的?数据如何在各级存储间流动?如何让计算和通信重叠?只有把这些问题想清楚,才能写出真正发挥硬件威力的代码。BANG的成功,为我们在GPU上实现其他复杂图算法或数据密集型应用,提供了非常宝贵的范式和灵感。
