文章目录
- PART \textbf{PART } PART Ⅰ: 导论与预备知识
- 1. \textbf{1. } 1. 导论
- 2. GPU \textbf{2. GPU} 2. GPU架构与 CUDA \textbf{CUDA} CUDA编程模型
- PART \textbf{PART } PART Ⅱ: BANG \textbf{BANG} BANG的设计
- 1. BANG \textbf{1. BANG} 1. BANG的总体设计
- 2. BANG \textbf{2. BANG} 2. BANG的微内核设计与并行优化
- 2.0. \textbf{2.0. } 2.0. 微内核总体设计概览
- 2.1. \textbf{2.1. } 2.1. 第一阶段: PQ \textbf{PQ} PQ表的构建微内核
- 2.2. {}{}\textbf{2.2. } 2.2. 第二阶段: 主循环的主要内核 + \textbf{+} +并行优化
- 2.2.1. \textbf{2.2.1. } 2.2.1. 中 GPU \textbf{GPU} GPU阶段的 GPU \textbf{GPU} GPU微内核
- 2.2.1.1. \textbf{2.2.1.1. } 2.2.1.1. Bloom Filter \textbf{Bloom Filter} Bloom Filter微内核: 为 N i N_i Ni过滤已访问点
- 2.2.1.2. \textbf{2.2.1.2. } 2.2.1.2. 并行距离计算微内核: 计算 N i ′ {}N_i' Ni′集中所有邻居离查询点的距离
- 2.2.1.3. \textbf{2.2.1.3. } 2.2.1.3. 并行归并排序微内核: 为 N i ′ N_i^{'} Ni′集中所有邻居排序
- 2.2.1.4. \textbf{2.2.1.4. } 2.2.1.4. 列表合并微内核: 合并已排序的 N i ′ \mathcal{N}_i^{'} Ni′与当前工作列表 L i \mathcal{L}_i Li
- 2.2.2. \textbf{2.2.2. } 2.2.2. 中 GPU \textbf{GPU} GPU阶段的(非内核)并行优化
- 2.3. \textbf{2.3. } 2.3. 第三阶段: 实现重排的微内核
- 3. BANG \textbf{3. BANG} 3. BANG的不同版本
- PART \textbf{PART } PART Ⅲ: 实验验证与结论
- 1. \textbf{1. } 1. 实验设置
- 2. \textbf{2. } 2. 实验结果
- 3. \textbf{3. } 3. 结论
原论文: BANG: Billion-Scale Approximate Nearest Neighbor Search using a Single GPU
PART \textbf{PART } PART Ⅰ: 导论与预备知识
1. \textbf{1. } 1. 导论
1.1. \textbf{1.1. } 1.1. 关于 ANN \textbf{ANN} ANN
1️⃣高维 k k k最邻近查询
- 精确查询 (NN) \text{(NN)} (NN):
- 含义:找到与给定查询点最近的 k k k个数据点
- 困难:由于维度诅咒 → \to →难以摆脱暴力扫描 O ( n ∗ dimension ) O(n*\text{dimension}) O(n∗dimension)的复杂度
- 近似查询 (ANN) \text{(ANN)} (ANN):
- 核心:通过牺牲准确性来换取速度,以减轻维度诅咒
- On GPU \text{On GPU} On GPU:大规模并行处理可以提高 ANN \text{ANN} ANN吞吐量(固定时间内的查询数量)
- 基于图的 ANN \text{ANN} ANN:
- 处理大规模数据最为高效的 ANN \text{ANN} ANN方法
- Vamana/DiskANN \text{Vamana/DiskANN} Vamana/DiskANN是目前最先进的基于图的 ANN \text{ANN} ANN(详细的设计 Click Here \text{Click Here} Click Here)
1.2. \textbf{1.2. } 1.2. ANN \textbf{ANN} ANN的 GPU \textbf{GPU} GPU实现难点
1️⃣ GPU \text{GPU} GPU的内存有限
- 含义:目前主流 GPU \text{GPU} GPU内存有限,无法将构建好的图结构完整载入
- 现有方案:
方案 描述 缺陷 文献 分片 将图分片 → \to →不断在 CPU ⇆ GPU \text{CPU}\leftrightarrows{}\text{GPU} CPU⇆GPU交换片以处理整个图 PCIe \text{PCIe} PCIe带宽不够 GGNN \text{GGNN} GGNN 多 GPU \text{GPU} GPU 将图有效分割到所有 GPU \text{GPU} GPU上以容纳并处理整个图 硬件成本高 SONG \text{SONG} SONG/ FAISS \text{FAISS} FAISS 压缩 压缩图数据维度使图结构能北方进 GPU \text{GPU} GPU内存 召回率下降(只适合小数据) GGNN \text{GGNN} GGNN 2️⃣最有硬件使用
- GPU ⇆ CPU \text{GPU}\leftrightarrows{}\text{CPU} GPU⇆CPU负载平衡:确保二者持续并行工作不空闲,并且数据传输量不超过 PCIe \text{PCIe} PCIe极限
- 主存占用:基于 GPU \text{GPU} GPU的 ANN \text{ANN} ANN搜索占用的内存显著增加
1.3. BANG \textbf{1.3. BANG} 1.3. BANG的总体优化思路
1️⃣硬件优化
- 总线优化:减少 CPU-GPU \text{CPU-GPU} CPU-GPU间 PCIe \text{PCIe} PCIe的通信量 → \to →提高吞吐
优化思路 具体措施 减少(总共的)总线传输次数 负载平衡,预取/流水线(让 CPU/GPU \text{CPU/GPU} CPU/GPU尽量没空闲时间) 降低(一次的)总线传输量 传输 PQ \text{PQ} PQ压缩后的向量(而非原始向量) - GPU \text{GPU} GPU内存优化:避免存放图结构 + + +只存放 PQ \text{PQ} PQ压缩后的向量
2️⃣计算优化
- 加速遍历/搜索:使用 Bloom \text{Bloom} Bloom过滤器,快速判断 a ∈ A a\text{∈}A a∈A式命题的真伪
- 加速距离计算:使用 PQ \text{PQ} PQ压缩后的向量计算距离
3️⃣软件优化:设立微内核,将距离计算/排序/更新表操作拆分成更原子化的操作,以提高并行化
2. GPU \textbf{2. GPU} 2. GPU架构与 CUDA \textbf{CUDA} CUDA编程模型
2.1. GPU \textbf{2.1. }\textbf{GPU} 2.1. GPU体系结构
1️⃣计算单元组织架构
![]()
结构 功能 CUDA \text{CUDA} CUDA核心 类似 ALU \text{ALU} ALU(但远没 CPU \text{CPU} CPU的灵活),可执行浮点运算/张量运算/光线追踪(高级核心) Warp \text{Warp} Warp 多核心共用一个取指/译码器,按 SIMT \text{SIMT} SIMT工作(所有线程指令相同/数据可不同) SM \text{SM} SM 包含多组 Warps \text{Warps} Warps,所有 CUDA \text{CUDA} CUDA核心共用一套执行上下文(缓存) & \& &共享内存 2️⃣存储层次架构:
![]()
- 不同 SM \text{SM} SM能够 Access \text{Access} Access相同的 L2 Cache \text{L2 Cache} L2 Cache
- 显存与缓存之间的带宽极高,但是相比 GPU \text{GPU} GPU的运算能力仍然有瓶颈
2.2. \textbf{2.2. } 2.2. CUDA \textbf{CUDA} CUDA编程模型
1️⃣ CUDA \text{CUDA} CUDA程序简述
- CUDA \text{CUDA} CUDA程序的两部分
程序 运行位置 主要职责 Host
程序CPU \text{CPU} CPU 任务管理/数据传输/启动 GPU \text{GPU} GPU内核 Device
程序GPU \text{GPU} GPU 执行内核/处理数据 - Kernel \text{Kernel} Kernel即在 GPU \text{GPU} GPU上运行的函数,如下简单内核定义示例
//通过__global__关键字声名内核函数 __global__ void VecAdd(float* A, float* B, float* C) { int i = threadIdx.x; C[i] = A[i] + B[i]; } int main() { //通过<<<...>>>中参数指定执行kernel的CUDA thread数量 VecAdd<<<1, N>>>(A, B, C); }
2️⃣线程并行执行架构
- 线程层次:
结构 地位 功能 Thread \text{Thread} Thread 并行执行最小单元 执行 Kernel \text{Kernel} Kernel的一段代码 Warp(32Threads) \text{Warp(32Threads)} Warp(32Threads) 线程调度的基本单位 所有线程以 SIMD \text{SIMD} SIMD方式执行相同指令 Block \text{Block} Block GPU \text{GPU} GPU执行线程基本单位 使块内线程内存共享/指令同步 Grid \text{Grid} Grid 并行执行的最大单元 执行整个内核(启动内核时必启动整个 Grid \text{Grid} Grid) - 线程在计算单元的映射:线程层次 ↔ 层次对应 GPU \xleftrightarrow{层次对应}\text{GPU} 层次对应 GPU物理架构
![]()
- 注意 SM \text{SM} SM和 Block \text{Block} Block不必 1v1 \text{1v1} 1v1对应也可 Nv1 \text{Nv1} Nv1对应
- 线程在存储单元的映射
线程结构 可 Access \textbf{Access} Access的内存结构 访问速度 Thread \text{Thread} Thread 每线程唯一的 Local Memory \text{Local Memory} Local Memory 极快 Block \text{Block} Block 每块唯一的 Shared Memory \text{Shared Memory} Shared Memory(块中每个线程都可访问) 较快 所有线程 唯一且共享的 Global Memory \text{Global Memory} Global Memory 较慢 2.3. CPU \textbf{2.3. CPU} 2.3. CPU与 GPU \textbf{GPU} GPU
![]()
1️⃣ CPU/GPU \text{CPU/}\text{GPU} CPU/GPU结构对比
GPU \text{GPU} GPU CPU \text{CPU} CPU ALU \text{ALU} ALU 功能强但数量少(只占 GPU \text{GPU} GPU小部),时钟频率极高 功能弱但数量大,时钟频率低 Cache \text{Cache} Cache 容量大并分级,缓存后续访问数据 容量很小,用于提高线程服务 控制 复杂串行逻辑,如流水/分支预测/乱序执行 简单(但大规模)并行逻辑 3️⃣ CPU ↔ 数据 / 指令传输 PCIe GPU \text{CPU} \xleftrightarrow[数据/指令传输]{\text{PCIe}} \text{GPU} CPUPCIe 数据/指令传输GPU交互
设备 逻辑地位 IO \textbf{IO} IO模块 任务分配 GPU \text{GPU} GPU 外设 IO Block \text{IO Block} IO Block(南桥) 控制逻辑和任务调度 CPU \text{CPU} CPU 主机 Copy Engine \text{Copy Engine} Copy Engine 执行大量并行计算任务
PART \textbf{PART } PART Ⅱ: BANG \textbf{BANG} BANG的设计
1. BANG \textbf{1. BANG} 1. BANG的总体设计
1.1. BANG \textbf{1.1. BANG} 1.1. BANG的索引架构
1.1.1. \textbf{1.1.1. } 1.1.1. BANG \textbf{BANG} BANG索引(硬件)布局
结构 功能 RAM \text{RAM} RAM 存放 Vamana \text{Vamana} Vamana算法构建的图结构 + + +数据点 GPU \text{GPU} GPU内存 存放 Vamana \text{Vamana} Vamana算法构建的图中点经过 PQ \text{PQ} PQ压缩后的向量 CPU-GPU \text{CPU-GPU} CPU-GPU总线 传输压缩向量 & \& &协调并行 1.1.2. BANG \textbf{1.1.2. BANG} 1.1.2. BANG索引构建算法: Vamana \textbf{Vamana} Vamana图
1️⃣ Vamana \text{Vamana} Vamana图构建基本操作
- 图查询算法:贪心搜索 GreedySearch ( s , x q , k , L ) \text{GreedySearch} \left(s, \mathrm{x}_q, k, L\right) GreedySearch(s,xq,k,L)
- 图剪枝算法:健壮性剪枝 RobustPrune ( p , R , α , L ) \text{RobustPrune}(p, R, \alpha, L) RobustPrune(p,R,α,L)
2️⃣ Vamana \text{Vamana} Vamana图构建总体流程
![]()
1.1.3. BANG \textbf{1.1.3. BANG} 1.1.3. BANG索引构建方法: 类似 DiskANN \textbf{DiskANN} DiskANN架构
1️⃣构建步骤:面向面向内存空间的优化
- 划分:用 k -means k\text{-means} k-means将 P P P分为多簇(每簇有一中心),再将 P P P所有点分给 ℓ > 1 \ell\text{>}1 ℓ>1个中心以构成重叠簇
- 索引:在每个重叠簇中执行 Vamana \text{Vamana} Vamana算法,构建相应有向边
- 合并:将所有构建的有向边合并在一个图中,完成构建
2️⃣关于重叠分簇:为了保证图的连通性,以及后续搜索的 Navigable \text{Navigable} Navigable
1.2. BANG \textbf{1.2. BANG} 1.2. BANG的查询架构
1.2.1. \textbf{1.2.1. } 1.2.1. 第一阶段: 初始化 &PQ \textbf{\&PQ} &PQ表的构建
1️⃣执行的操作
- 并行化:为查询集 Q ρ Q_\rho Qρ中的每个查询 { q 1 , q 2 , . . . , q ρ } \{q_1,q_2,...,q_{\rho}\} {q1,q2,...,qρ}分配一个独立的 CUDA \text{CUDA} CUDA线程 Block \text{Block} Block
- 距离表:在每个线程块上为每个 q i q_i qi计算并构建 PQ \text{PQ} PQ距离子表,最终合并 ρ \rho ρ个子表为距离表
- 搜索起点:每个 q i q_i qi从图质心开始,即 CPU ← 传输给 WorkList ← 放入 u i ∗ (当前/候选点) ← 初始化 Centroid \text{CPU}\xleftarrow{传输给}\text{WorkList}\xleftarrow{放入}\textbf{u}_i^*\textbf{(当前/候选点)}\xleftarrow{初始化}\text{Centroid} CPU传输给WorkList放入ui∗(当前/候选点)初始化Centroid
2️⃣ PQ \text{PQ} PQ表构建的时序逻辑
时期 操作 查询开始前 将查询点送入 GPU \text{GPU} GPU的 Copy \text{Copy} Copy引擎,在 CUDA \text{CUDA} CUDA核心上计算/构建/存储距离表 查询开始后 保留距离表在 GPU \text{GPU} GPU上直到查询结束 1.2.2. \textbf{1.2.2. } 1.2.2. 第二阶段: 并行 GreedySearch \textbf{GreedySearch} GreedySearch主循环
![]()
1️⃣前 CPU \text{CPU} CPU阶段: CPU \text{CPU} CPU从内存中获取当前在处理节点 u i ∗ u_i^* ui∗的邻居集 N i N_i Ni
🔁数据传输: CPU → 邻居集 N i GPU \text{CPU}\xrightarrow{邻居集N_i}\text{GPU} CPU邻居集NiGPU
2️⃣中 GPU \text{GPU} GPU阶段:接收 u i ∗ u_i^* ui∗的邻居集 N i N_i Ni后,并行地执行内核 & \text{\&} &全精度向量的异步传输
- 执行内核:按顺序执行以下内核及操作
步骤 操作 内核与否 过滤邻居 用 Bloom \text{Bloom} Bloom并行检查 ∀ n ∈ N i \forall{}n\text{∈}N_i ∀n∈Ni中未被访问点 → \to →并放入 N i ′ {}N_i' Ni′(未访问集) + \text{+} +更新 Bloom \text{Bloom} Bloom ✔️ 距离计算 用 PQ \text{PQ} PQ距离表并行计算所有未处理邻居 n k ∈ N i ′ {}n_k\text{∈}N_i' nk∈Ni′与查询点 q i q_i qi距离,并存在 D i [ k ] \mathcal{D}_i[k] Di[k] ✔️ 邻居排序 将 N i ′ {}N_i' Ni′和 D i [ k ] \mathcal{D}_i[k] Di[k]按与 q i q_i qi的距离执行归并排序,得到排序后的距离 D i ′ \mathcal{D}_i' Di′和节点 N i ′ \mathcal{N}_i' Ni′ ✔️ 合并列表 合并当前 WorkLisk ( L i ) \text{WorkLisk}(\mathcal{L}_i) WorkLisk(Li)与新排序的节点列表 N i ′ \mathcal{N}_i' Ni′形成新的 L i \mathcal{L}_i Li ✔️ 更新节点 又将 L i \mathcal{L}_i Li排序后选取最近的未访问点 u i ∗ {}u_i^* ui∗作为下一个当前节点 ❌ - 异步传输:执行内核的同时, CPU \text{CPU} CPU将 u i ∗ u_i^* ui∗的全精度向量传输给 GPU \text{GPU} GPU → \to →以便后续重排
🔁数据传输: CPU ← 当前节点 u i ∗ GPU \text{CPU}\xleftarrow{当前节点u_i^*}\text{GPU} CPU当前节点ui∗GPU
3️⃣后 CPU \text{CPU} CPU阶段:若 L i \mathcal{L}_i Li中所有点都被访问过且 ∣ L i ∣ = t |\mathcal{L}_i|\text{=}t ∣Li∣=t,则认为已经收敛 → \to →结束循环
1.2.3. \textbf{1.2.3. } 1.2.3. 第三阶段: (搜索收敛后的)重排与输出
1️⃣重排与输出
- 重排的时序逻辑
时间 操作 位置 搜索过程中 用一个数据结构,存储每个 Iter \text{Iter} Iter中向 CPU \text{CPU} CPU发送的全精度候选点 CPU→GPU \text{CPU→GPU} CPU→GPU 搜索完成后 计算所有候选点到查询点距离,按全精度距离排序后选取前若干 GPU \text{GPU} GPU - 输出:选取重排后的 L i \mathcal{L}_i Li中,离 q i q_i qi最近的 k k k个节点 → \to →作为 k - k\text{-} k-最邻近返回
2️⃣重排的意义:用小成本(仅极小部分即候选点以全精度送往 GPU \text{GPU} GPU),补偿由压缩距离产生的误差
2. BANG \textbf{2. BANG} 2. BANG的微内核设计与并行优化
2.0. \textbf{2.0. } 2.0. 微内核总体设计概览
1️⃣设立独立微内核的操作:
阶段 有独立微内核的操作 第一阶段(建表) PQ \text{PQ} PQ表构建操作 第二阶段(主查询) 过滤邻居,距离计算,邻居(归并)排序,归并列表 第三阶段(重排) 重排操作 2️⃣动态线程块的优化:
- 每个查询分配到一线程块执行,查询过程会依次执行多个内核
- 执行不同内核时按经验调整线程块大小(如计算密集型内核的块更大),以保证 GPU \text{GPU} GPU的高占有
2.1. \textbf{2.1. } 2.1. 第一阶段: PQ \textbf{PQ} PQ表的构建微内核
2.1.1. \textbf{2.1.1. } 2.1.1. 关于向量压缩的 PQ \textbf{PQ} PQ办法
1️⃣ k -Means k\text{-Means} k-Means分簇方法
- 含义:一种无监督学习,用于将数据集分为 k k k个簇(每簇一个质心),使同簇点靠近/异簇点远离
- 流程:
2️⃣ PQ \text{PQ} PQ算法流程
- 给定 k k k个 D D D维向量
{ v 1 = [ x 11 , x 12 , x 13 , x 14 , . . . , x 1 D ] v 2 = [ x 21 , x 22 , x 23 , x 24 , . . . , x 2 D ] . . . . . . . . . v k = [ x k 1 , x k 2 , x k 3 , x k 4 , . . . , x a D ] ↔ { v 1 = { [ x 11 , x 12 , x 13 ] , [ x 14 , x 15 , x 16 ] , . . . , [ x 1 ( D − 1 ) , x 1 ( D − 1 ) , x 1 D ] } v 2 = { [ x 21 , x 22 , x 23 ] , [ x 24 , x 25 , x 26 ] , . . . , [ x 2 ( D − 1 ) , x 2 ( D − 1 ) , x 2 D ] } . . . . . . . . . v k = { [ x k 1 , x k 2 , x k 3 ] , [ x k 4 , x k 5 , x k 6 ] , . . . , [ x k ( D − 1 ) , x k ( D − 1 ) , x k D ] } \begin{cases} \textbf{v}_1=[x_{11},x_{12},x_{13},x_{14},...,x_{1D}]\\\\ \textbf{v}_2=[x_{21},x_{22},x_{23},x_{24},...,x_{2D}]\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_k=[x_{k1},x_{k2},x_{k3},x_{k4},...,x_{aD}] \end{cases}\xleftrightarrow{} \begin{cases} \textbf{v}_{1}=\{[x_{11},x_{12},x_{13}],[x_{14},x_{15},x_{16}],...,[x_{1(D-1)},x_{1(D-1)},x_{1D}]\}\\\\ \textbf{v}_{2}=\{[x_{21},x_{22},x_{23}],[x_{24},x_{25},x_{26}],...,[x_{2(D-1)},x_{2(D-1)},x_{2D}]\}\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_{k}=\{[x_{k1},x_{k2},x_{k3}],[x_{k4},x_{k5},x_{k6}],...,[x_{k(D-1)},x_{k(D-1)},x_{kD}]\} \end{cases} ⎩ ⎨ ⎧v1=[x11,x12,x13,x14,...,x1D]v2=[x21,x22,x23,x24,...,x2D].........vk=[xk1,xk2,xk3,xk4,...,xaD] ⎩ ⎨ ⎧v1={[x11,x12,x13],[x14,x15,x16],...,[x1(D−1),x1(D−1),x1D]}v2={[x21,x22,x23],[x24,x25,x26],...,[x2(D−1),x2(D−1),x2D]}.........vk={[xk1,xk2,xk3],[xk4,xk5,xk6],...,[xk(D−1),xk(D−1),xkD]}- 分割子空间:将 D D D维向量分为 M M M个 D M \cfrac{D}{M} MD维向量
子空间 1 { v 11 = [ x 11 , x 12 , x 13 ] v 21 = [ x 21 , x 22 , x 23 ] . . . . . . . . . v k 1 = [ x k 1 , x k 2 , x k 3 ] & 子空间 2 { v 12 = [ x 14 , x 15 , x 16 ] v 22 = [ x 24 , x 25 , x 26 ] . . . . . . . . . v k 2 = [ x k 4 , x k 5 , x k 6 ] & . . . & 子空间 M { v 1 M = [ x 1 ( D − 1 ) , x 1 ( D − 1 ) , x 1 D ] v 2 M = [ x 2 ( D − 1 ) , x 2 ( D − 1 ) , x 2 D ] . . . . . . . . . v k M = [ x k ( D − 1 ) , x k ( D − 1 ) , x k D ] 子空间1\begin{cases} \textbf{v}_{11}=[x_{11},x_{12},x_{13}]\\\\ \textbf{v}_{21}=[x_{21},x_{22},x_{23}]\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_{k1}=[x_{k1},x_{k2},x_{k3}] \end{cases}\&子空间2 \begin{cases} \textbf{v}_{12}=[x_{14},x_{15},x_{16}]\\\\ \textbf{v}_{22}=[x_{24},x_{25},x_{26}]\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_{k2}=[x_{k4},x_{k5},x_{k6}] \end{cases}\&...\&子空间M \begin{cases} \textbf{v}_{1M}=[x_{1(D-1)},x_{1(D-1)},x_{1D}]\\\\ \textbf{v}_{2M}=[x_{2(D-1)},x_{2(D-1)},x_{2D}]\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_{kM}=[x_{k(D-1)},x_{k(D-1)},x_{kD}] \end{cases} 子空间1⎩ ⎨ ⎧v11=[x11,x12,x13]v21=[x21,x22,x23].........vk1=[xk1,xk2,xk3]&子空间2⎩ ⎨ ⎧v12=[x14,x15,x16]v22=[x24,x25,x26].........vk2=[xk4,xk5,xk6]&...&子空间M⎩ ⎨ ⎧v1M=[x1(D−1),x1(D−1),x1D]v2M=[x2(D−1),x2(D−1),x2D].........vkM=[xk(D−1),xk(D−1),xkD]- 生成 PQ \text{PQ} PQ编码:
子空间 1 { v 11 ← 替代 Centriod 11 v 21 ← 替代 Centriod 21 . . . . . . . . . v k 1 ← 替代 Centriod k 1 & 子空间 2 { v 12 ← 替代 Centriod 12 v 22 ← 替代 Centriod 22 . . . . . . . . . v k 2 ← 替代 Centriod k 2 & . . . & 子空间 M { v 1 M ← 替代 Centriod 1 M v 2 M ← 替代 Centriod 2 M . . . . . . . . . v k M ← 替代 Centriod k M 子空间1\begin{cases} \textbf{v}_{11}\xleftarrow{替代}\text{Centriod}_{11}\\\\ \textbf{v}_{21}\xleftarrow{替代}\text{Centriod}_{21}\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_{k1}\xleftarrow{替代}\text{Centriod}_{k1} \end{cases}\&子空间2 \begin{cases} \textbf{v}_{12}\xleftarrow{替代}\text{Centriod}_{12}\\\\ \textbf{v}_{22}\xleftarrow{替代}\text{Centriod}_{22}\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_{k2}\xleftarrow{替代}\text{Centriod}_{k2} \end{cases}\&...\&子空间M \begin{cases} \textbf{v}_{1M}\xleftarrow{替代}\text{Centriod}_{1M}\\\\ \textbf{v}_{2M}\xleftarrow{替代}\text{Centriod}_{2M}\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_{kM}\xleftarrow{替代}\text{Centriod}_{kM} \end{cases} 子空间1⎩ ⎨ ⎧v11替代Centriod11v21替代Centriod21.........vk1替代Centriodk1&子空间2⎩ ⎨ ⎧v12替代Centriod12v22替代Centriod22.........vk2替代Centriodk2&...&子空间M⎩ ⎨ ⎧v1M替代Centriod1Mv2M替代Centriod2M.........vkM替代CentriodkM
- 聚类:在每个子空间上运行 k -Means k\text{-Means} k-Means算法(一般 k = 256 k\text{=}256 k=256) → \to →每个 v i j \textbf{v}_{ij} vij都会分到一个 D M \cfrac{D}{M} MD维的质心
- 编码:将每个子向量 v i j \textbf{v}_{ij} vij所属质心的索引作为其 PQ \text{PQ} PQ编码,并替代原有子向量
- 生成最终的压缩向量 → { v 1 ~ = { Centriod 11 , Centriod 12 , . . . , Centriod 1 M } v 2 ~ = { Centriod 21 , Centriod 22 , . . . , Centriod 2 M } . . . . . . . . . v k ~ = { Centriod k 1 , Centriod k 2 , . . . , Centriod k M } \to\begin{cases} \widetilde{\textbf{v}_{1}}=\{\text{Centriod}_{11},\text{Centriod}_{12},...,\text{Centriod}_{1M}\}\\\\ \widetilde{\textbf{v}_{2}}=\{\text{Centriod}_{21},\text{Centriod}_{22},...,\text{Centriod}_{2M}\}\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \widetilde{\textbf{v}_{k}}=\{\text{Centriod}_{k1},\text{Centriod}_{k2},...,\text{Centriod}_{kM}\} \end{cases} →⎩ ⎨ ⎧v1 ={Centriod11,Centriod12,...,Centriod1M}v2 ={Centriod21,Centriod22,...,Centriod2M}.........vk ={Centriodk1,Centriodk2,...,CentriodkM}
2.1.2. \textbf{2.1.2. } 2.1.2. PQ \textbf{PQ} PQ表的构建内核设计
1️⃣ PQ \text{PQ} PQ压缩:将原有 D D D维向量分为 M M M个子空间,每个子空间 k -Means k\text{-Means} k-Means聚类出 k k k个簇/质心
- { 数据点: { v 1 = [ x 11 , x 12 , x 13 , x 14 , . . . , x 1 D ] v 2 = [ x 21 , x 22 , x 23 , x 24 , . . . , x 2 D ] . . . . . . . . . v α = [ x α 1 , x α 2 , x α 3 , x α 4 , . . . , x α D ] . . . . . . . . . v k = [ x k 1 , x k 2 , x k 3 , x k 4 , . . . , x k D ] → 分割 PQ { v 1 ~ = { Centriod 11 , Centriod 12 , . . . , Centriod 1 M } v 2 ~ = { Centriod 21 , Centriod 22 , . . . , Centriod 2 M } . . . . . . . . . v α ~ = { Centriod α 1 , Centriod α 2 , . . . , Centriod α M } . . . . . . . . . v k ~ = { Centriod k 1 , Centriod k 2 , . . . , Centriod k M } 查询点: q = [ q 1 , q 2 , q 3 , q 4 , . . . , q D ] → 分割 与PQ子空间的维度划分对齐 q = { q 1 , q 2 , . . . , q M } \begin{cases}数据点\text{: }\begin{cases} \textbf{v}_1=[x_{11},x_{12},x_{13},x_{14},...,x_{1D}]\\\\ \textbf{v}_2=[x_{21},x_{22},x_{23},x_{24},...,x_{2D}]\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_α=[x_{α1},x_{α2},x_{α3},x_{α4},...,x_{αD}]\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \textbf{v}_k=[x_{k1},x_{k2},x_{k3},x_{k4},...,x_{kD}] \end{cases} \xrightarrow[分割]{\text{PQ}}\begin{cases} \widetilde{\textbf{v}_{1}}=\{\textbf{Centriod}_{11},\textbf{Centriod}_{12},...,\textbf{Centriod}_{1M}\}\\\\ \widetilde{\textbf{v}_{2}}=\{\textbf{Centriod}_{21},\textbf{Centriod}_{22},...,\textbf{Centriod}_{2M}\}\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \widetilde{\textbf{v}_{α}}=\{\textbf{Centriod}_{α1},\textbf{Centriod}_{α2},...,\textbf{Centriod}_{αM}\}\\\\ \,\,\,\,\,\,\,\,\,\,\,\,.........\\\\ \widetilde{\textbf{v}_{k}}=\{\textbf{Centriod}_{k1},\textbf{Centriod}_{k2},...,\textbf{Centriod}_{kM}\} \end{cases}\\\\ 查询点\text{: }\textbf{q}=[q_{1},q_{2},q_{3},q_{4},...,q_{D}]\xrightarrow[分割]{与\text{PQ}子空间的维度划分对齐}\textbf{q}=\{\textbf{q}_1,\textbf{q}_2,...,\textbf{q}_M\} \end{cases} ⎩ ⎨ ⎧数据点: ⎩ ⎨ ⎧v1=[x11,x12,x13,x14,...,x1D]v2=[x21,x22,x23,x24,...,x2D].........vα=[xα1,xα2,xα3,xα4,...,xαD].........vk=[xk1,xk2,xk3,xk4,...,xkD]PQ分割⎩ ⎨ ⎧v1 ={Centriod11,Centriod12,...,Centriod1M}v2 ={Centriod21,Centriod22,...,Centriod2M}.........vα ={Centriodα1,Centriodα2,...,CentriodαM}.........vk ={Centriodk1,Centriodk2,...,CentriodkM}查询点: q=[q1,q2,q3,q4,...,qD]与PQ子空间的维度划分对齐分割q={q1,q2,...,qM}
2️⃣线程映射
- 到线程块:每个 q i q_i qi构建 PQ \text{PQ} PQ距离子表的操作分给一个线程块
- 到单线程:每个子空间对应一个独立线程,依次计算 q s ∈ R D M q_s \text{∈} \mathbb{R}^{^{\frac{D}{M}}} qs∈RMD与每个 k k k个质心的距离
3️⃣构建操作
- 构建子表:在每个子空间中计算查询点 q s ∈ R D M q_s \text{∈} \mathbb{R}^{^{\frac{D}{M}}} qs∈RMD ↔ Euclidean距离平方 \xleftrightarrow{\text{Euclidean距离平方}} Euclidean距离平方 所有簇的质心 → 得到 \xrightarrow{得到} 得到 M × k M\text{×}k M×k维子表,一线程负责一子空间
Dist. Tab. \text{Dist. Tab.} Dist. Tab. 子空间/线程 1 1 1 子空间/线程 2 2 2 … 子空间/线程 M M M v 1 \textbf{v}_1 v1 dist 2 ( q 1 , Centriod 11 ) \text{dist}^2(\textbf{q}_1,\textbf{Centriod}_{11}) dist2(q1,Centriod11) dist 2 ( q 2 , Centriod 12 ) \text{dist}^2(\textbf{q}_2,\textbf{Centriod}_{12}) dist2(q2,Centriod12) … dist 2 ( q M , Centriod 1 M ) \text{dist}^2(\textbf{q}_M,\textbf{Centriod}_{1M}) dist2(qM,Centriod1M) v 2 \textbf{v}_2 v2 dist 2 ( q 1 , Centriod 21 ) \text{dist}^2(\textbf{q}_1,\textbf{Centriod}_{21}) dist2(q1,Centriod21) dist 2 ( q 2 , Centriod 22 ) \text{dist}^2(\textbf{q}_2,\textbf{Centriod}_{22}) dist2(q2,Centriod22) … dist 2 ( q M , Centriod 2 M ) \text{dist}^2(\textbf{q}_M,\textbf{Centriod}_{2M}) dist2(qM,Centriod2M) … … … … … v k \textbf{v}_k vk dist 2 ( q 1 , Centriod k 1 ) \text{dist}^2(\textbf{q}_1,\textbf{Centriod}_{k1}) dist2(q1,Centriodk1) dist 2 ( q 2 , Centriod k 2 ) \text{dist}^2(\textbf{q}_2,\textbf{Centriod}_{k2}) dist2(q2,Centriodk2) … dist 2 ( q M , Centriod k M ) \text{dist}^2(\textbf{q}_M,\textbf{Centriod}_{kM}) dist2(qM,CentriodkM) - 合并子表:将 ρ \rho ρ个 M × k M\text{×}k M×k维的子表合并为最终 ρ × M × k \rho\text{×}M\text{×}k ρ×M×k维表,并存储在 GPU \text{GPU} GPU内存上直到查询结束
4️⃣一些说明 & \& &算法分析
- 距离计算原理: dist ( q , v α ) = ∑ i = 1 M dist 2 ( q i , Centriod α i ) \text{dist}(q,\textbf{v}_α)=\displaystyle{}\sum\limits_{i=1}^{M}\text{dist}^2(q_i,\textbf{Centriod}_{αi}) dist(q,vα)=i=1∑Mdist2(qi,Centriodαi),就是表中 v α \textbf{v}_α vα行所有内容相加
- 参数设定:更具经验设定 k = 256 k\text{=}256 k=256,由消融实验确定 M = 74 M\text{=}74 M=74最优
- 算法分析:
Item \textbf{Item} Item 含义 复杂度 Work \text{Work} Work 算法串行执行总耗时 O ( ( m ⋅ subspace_size ) ⋅ 256 ⋅ ρ ) O((m \cdot \text{subspace\_size}) \cdot 256 \cdot \rho) O((m⋅subspace_size)⋅256⋅ρ) Span \text{Span} Span 算法并行执行耗时,即最耗时串行步骤耗时 O ( m ⋅ subspace_size ) = O ( d ) O(m \cdot \text{subspace\_size}) = O(d) O(m⋅subspace_size)=O(d) 2.2. {}{}\textbf{2.2. } 2.2. 第二阶段: 主循环的主要内核 + \textbf{+} +并行优化
2.2.1. \textbf{2.2.1. } 2.2.1. 中 GPU \textbf{GPU} GPU阶段的 GPU \textbf{GPU} GPU微内核
2.2.1.1. \textbf{2.2.1.1. } 2.2.1.1. Bloom Filter \textbf{Bloom Filter} Bloom Filter微内核: 为 N i N_i Ni过滤已访问点
1️⃣一些背景
- 为何要过滤已访问点
- Vamana \text{Vamana} Vamana图具有建立远程边特性,搜索过程必定碰到很多相同点
- 如果不过滤邻居会导致大量重复计算,实验证明 Recall \text{Recall} Recall会下降至原有 0.1 0.1 0.1
- 传统已访问节点追踪方法
描述 弊端 为每个点多划出 1bit \text{1bit} 1bit以标记访问与否 对十亿级别点集,会造成百 GB \text{GB} GB级额外存储开销 用优先队列/哈希表存放已访问点 队列/哈希表等动态数据结构不利于 GPU \text{GPU} GPU的并行 2️⃣ Bloom \text{Bloom} Bloom过滤器的原理
![]()
- 组成结构:长为 z z z的布尔数组(初始化为全 0 0 0) + + + k k k个哈希函数
- 构建过程:给定集合 A A A
- 哈希:对 ∀ Input i ∈ A \forall{}\text{Input}_i\text{∈}A ∀Inputi∈A通过所有 k k k个哈希函数,得到 n × k n\text{×}k n×k个哈希值 f H a s h i ( Input j ) mod z f_{Hash}^{i}(\text{Input}_j)\text{ mod }z fHashi(Inputj) mod z
- 填充:将布尔数组中 index = f H a s h i ( Input j ) mod z \text{index}=f_{Hash}^{i}(\text{Input}_j)\text{ mod }z index=fHashi(Inputj) mod z的位由 0 0 0设为 1 1 1
💡如图中: f H a s h 3 ( Input 4 ) = 3 f_{Hash}^{3}(\text{Input}_4)\text{=}3 fHash3(Input4)=3所以设 array[3]=1 \text{array[3]=1} array[3]=1- 查询过程:给定元素 query \text{query} query
- 哈希:让 query \text{query} query通过所有 k k k个哈希函数,得到 k k k个哈希值 f H a s h i ( query ) mod z f_{Hash}^{i}(\text{query})\text{ mod }z fHashi(query) mod z
- 输出:若数组上所有索引值为 f H a s h i ( query ) mod z f_{Hash}^{i}(\text{query})\text{ mod }z fHashi(query) mod z 的 k k k个位置都是 1 1 1,则认为 query∈ A \text{query∈}A query∈A
💡如图中:如果 query \text{query} query的哈希值为 → { 1 / 3 / 4 → 认为query∈ A 1 / 2 / 4 → 不认为query∈A ( 源于array[2]=0) \small\to\begin{cases}1/3/4\to{}认为\text{query∈}A\\\\1/2/4\to{}不认为\text{query∈A}(源于\text{array[2]=0)}\end{cases} →⎩ ⎨ ⎧1/3/4→认为query∈A1/2/4→不认为query∈A(源于array[2]=0)3️⃣本文中 Bloom \text{Bloom} Bloom过滤器在的部署
- 哈希函数:采用两个 FNV1a \text{FNV1a} FNV1a,为非加密/轻量级
- 工作逻辑:
- 构建:将每轮迭代的当前(候选)点 u i ∗ {}u_i^* ui∗输入 Bloom \text{Bloom} Bloom过滤器以调整布尔数组
- 查询:过滤邻居阶段,将 u i ∗ u_i^* ui∗邻居集 N i N_i Ni全部输入 Bloom \text{Bloom} Bloom过滤器 → \to →排除已访问节点
2.2.1.2. \textbf{2.2.1.2. } 2.2.1.2. 并行距离计算微内核: 计算 N i ′ {}N_i' Ni′集中所有邻居离查询点的距离
1️⃣距离计算原理: dist ( q , v α ) \text{dist}(q,\textbf{v}_α) dist(q,vα)就是表中 v α \textbf{v}_α vα所有子距离相加,对应 M M M个子空间共 M M M项
Dist. Tab. \text{Dist. Tab.} Dist. Tab. 子空间 1 1 1 子空间 2 2 2 … 子空间 M M M … … … … … v α \textbf{v}_α vα dist 2 ( q 1 , Centriod α 1 ) \text{dist}^2(\textbf{q}_1,\textbf{Centriod}_{α1}) dist2(q1,Centriodα1) dist 2 ( q 2 , Centriod α 2 ) \text{dist}^2(\textbf{q}_2,\textbf{Centriod}_{α2}) dist2(q2,Centriodα2) … dist 2 ( q M , Centriod α M ) \text{dist}^2(\textbf{q}_M,\textbf{Centriod}_{αM}) dist2(qM,CentriodαM) … … … … … 2️⃣距离计算的并行实现
- 线程块的分组结构
![]()
- 线程块级别:为每个查询 q i ∈ Q ρ q_i\text{∈}Q_{\rho} qi∈Qρ分配一个独立的线程 Block \text{Block} Block
- 线程组级别:将所有线程分为 g g g组,每组线程数为 n := Sum_Threads_Num g n\text{:=}\cfrac{\text{{Sum\_Threads\_Num}}}{g} n:=gSum_Threads_Num
- 关于线程组:负责计算查询点 q i q_i qi与单个邻居的距离(故隐式要求 g > g\text{>} g>邻居总数)
![]()
- 计算:将 M M M个子空间分为 n {n} n组,组内每个线程并行地将自己组内的 M n \cfrac{M}{n} nM个子距离相加
- 寄存:组内每个线程将子距离相加的结果,直接放在其本地寄存器(避免了线程同步开销)
- 合并:组内各线程寄存结果相加 → 得到 \xrightarrow{得到} 得到查询点与邻居的距离,该步的两种 CUDA \text{CUDA} CUDA实现如下
CUDA \textbf{CUDA} CUDA函数 原理 备注 atomicAdd()
将所有结果累加到一个共享变量 实现简单,但不适用高并发情况 WarpReduce()
使用寄存器级规约 实验证明当 M = 74 M=74 M=74时性能略优 3️⃣一些说明 & \& &算法分析
- 该步骤时最耗时的内核:
- WorkList \text{WorkList} WorkList中邻居在 GPU \text{GPU} GPU内存的存储并非连续(未合并)
- 计算距离时需要频繁访存 GPU \text{GPU} GPU内存,而访问显存又极其耗时
- 算法分析: NumNbrs \text{NumNbrs} NumNbrs即邻居数目
Item \textbf{Item} Item 含义 复杂度 Work \text{Work} Work 算法串行执行总耗时 O ( NumNbrs ⋅ M ⋅ ρ ) O(\text{NumNbrs}\cdot M \cdot \rho) O(NumNbrs⋅M⋅ρ) Span \text{Span} Span 算法最耗时串行步骤耗时 O ( log M ) O(\log M) O(logM),源于 WarpReduce()
的二分规约2.2.1.3. \textbf{2.2.1.3. } 2.2.1.3. 并行归并排序微内核: 为 N i ′ N_i^{'} Ni′集中所有邻居排序
0️⃣归并排序流程
![]()
- 分割过程:将待排序数组等分为左右子数组,再对左右子数组递归式等分,直至不可分割
- 合并过程:将所有子数组两两递归合并,逐步得到较大有序数组,直到得到完整有序数组
1️⃣传统的并行归并
- 线程映射:为每个合并操作分配一个线程
- 问题所在:随着归并的进行 → { 同时合并的数组减少 → 并行工作的线程减少 单次合并的数组更长 → 单线程运行时间变长 → \small\to\begin{cases}同时合并的数组减少\to{}并行工作的线程减少\\\\单次合并的数组更长\to{}单线程运行时间变长\end{cases}\to →⎩ ⎨ ⎧同时合并的数组减少→并行工作的线程减少单次合并的数组更长→单线程运行时间变长→导致大量线程排序完成前空闲
2️⃣并行合并历程
- 线程映射:为合并操作中每个列表的每个元素都分配一个线程
- 并行合并历程:对于给定量已排序的待合并表 A A A与 B B B
![]()
- 线程索引: 为两列表中每个元素(邻居)分配一线程,线程索引 = = =元素在自己列表里的位置索引
- 位置计算:通过二分查找,找出两列表中每个元素插入对方列表后的索引
- 列表合并:将每个元素的两个索引相加即得元素在新的合并列表中的索引,由此得到合并列表
3️⃣基于并行合并历程的归并操作
- 线程映射:
- 单线程:为 N i ′ {}N_i' Ni′( u i ∗ u_i^{*} ui∗的未放问邻居集)中每个邻居(即合并时的每个元素)分配一个线程
- 线程块:为每个查询(即每个 u i ∗ u_i^{*} ui∗)分配一个线程块,块大小为 Vamana \text{Vamana} Vamana图节点最大邻居数
- 合并操作:从每个只有单元素的列表开始,依次执行并行合并历程
- 内存分配:由于最大邻居数的限制 → \to →排序列表长度较小,全程可将列表放在 GPU \text{GPU} GPU共享内存
4️⃣算法分析
- 对并行合并历程:令 ℓ \ell ℓ为列表长度
Item \textbf{Item} Item 含义 复杂度 Work \text{Work} Work 算法串行执行总耗时 O ( ℓ ⋅ log ( ℓ ) ) O(\ell \cdot \log (\ell)) O(ℓ⋅log(ℓ)) Span \text{Span} Span 算法最耗时串行步骤耗时 O ( log ( ℓ ) ) O(\log (\ell)) O(log(ℓ)),源于二分查找不可避免 - 对整体排序:对长为 n n n的数组,由递归得 T ( n ) = 2 ⋅ T ( n / 2 ) + n ⋅ log ( n ) ⇒ T ( n ) = O ( n ⋅ log 2 ( n ) ) T(n)=2 \cdot T(n / 2)+n \cdot \log (n)\Rightarrow{}T(n)=O\left(n \cdot \log ^2(n)\right) T(n)=2⋅T(n/2)+n⋅log(n)⇒T(n)=O(n⋅log2(n)),因此
Item \textbf{Item} Item 含义 复杂度 Work \text{Work} Work 算法串行执行总耗时 O ( NumNbrs ⋅ log 2 ( NumNbrs ) ⋅ ρ ) O\left(\text{NumNbrs} \cdot \log ^2(\text{NumNbrs}) \cdot \rho\right) O(NumNbrs⋅log2(NumNbrs)⋅ρ) Span \text{Span} Span 算法最耗时串行步骤耗时 O ( log 2 ( NumNbrs ) ) O\left(\log ^2(\text{NumNbrs}) \right) O(log2(NumNbrs)) 2.2.1.4. \textbf{2.2.1.4. } 2.2.1.4. 列表合并微内核: 合并已排序的 N i ′ \mathcal{N}_i^{'} Ni′与当前工作列表 L i \mathcal{L}_i Li
1️⃣合并操作:就是并行合并历程,可理解为上一步归并排序的附加步骤
![]()
2️⃣复杂度分析:令 ℓ \ell ℓ为列表长度
Item \textbf{Item} Item 含义 复杂度 Work \text{Work} Work 算法串行执行总耗时 O ( ℓ ⋅ log ( ℓ ) ) O(\ell \cdot \log (\ell)) O(ℓ⋅log(ℓ)) Span \text{Span} Span 算法最耗时串行步骤耗时 O ( log ( ℓ ) ) O(\log (\ell)) O(log(ℓ)),源于二分查找不可避免 2.2.2. \textbf{2.2.2. } 2.2.2. 中 GPU \textbf{GPU} GPU阶段的(非内核)并行优化
2.2.2.1. \textbf{2.2.2.1. } 2.2.2.1. 全精度向量的异步传输
1️⃣异步传输的基础
- 硬件基础:
- 主存:图在内存以点全精度向量 + + +邻居信息连续且定长存储组织 → \to →使可以顺序访问
- GPU \text{GPU} GPU内存:专设有一段内存,存储每次迭代异步传来的全精度向量,直到最后完成重排
- 软件基础:高级的 CUDA \text{CUDA} CUDA功能 → { 异步拷贝: 使数据传输时GPU可执行其它任务 CUDA流: 即CUDA核心可并行处理的指令序列 \small\to\begin{cases}异步拷贝\text{: }使数据传输时\text{GPU}可执行其它任务\\\\\text{CUDA}流\text{: }即\text{CUDA}核心可并行处理的指令序列\end{cases} →⎩ ⎨ ⎧异步拷贝: 使数据传输时GPU可执行其它任务CUDA流: 即CUDA核心可并行处理的指令序列
2️⃣异步传输的实现
- 传输的逻辑:
- 时序逻辑:两次 CPU ⇆ GPU \text{CPU}\leftrightarrows\text{GPU} CPU⇆GPU传输间,并行执行 → { CUDA核心: 执行计算任务 Copy引擎: 持续传递全精度向量 \small\to{}\begin{cases}\text{CUDA}核心\text{: }执行计算任务\\\\\text{Copy}引擎\text{: }持续传递全精度向量\end{cases} →⎩ ⎨ ⎧CUDA核心: 执行计算任务Copy引擎: 持续传递全精度向量
- 存取逻辑:得益于顺序存取,故启动异步传输时只需顺序移动指针就可获得全精度向量
- 相关的 CUDA \text{CUDA} CUDA函数:
CUDA \text{CUDA} CUDA函数 功能 cudaMemcpyAsync()
实现异步拷贝,即使得数据传输/计算同时进行 cudaStreamSynchronize()
实现数据依赖,即等必要数据传完后再执行有关计算/流 2.2.2.2. \textbf{2.2.2.2. } 2.2.2.2. 当前(候选)节点 u i ∗ u_i^{*} ui∗的预取
1️⃣优化的契机:避免 GPU \text{GPU} GPU与 CPU \text{CPU} CPU二者间存在过长的空闲
![]()
- 当 CPU \text{CPU} CPU在获取邻居时, GPU \text{GPU} GPU必定保持空闲
- 当 GPU \text{GPU} GPU在计算并更新 WorkList \text{WorkList} WorkList的时候, CPU \text{CPU} CPU有可能保持空闲
2️⃣候选(当前)节点的预取优化
![]()
- 时序逻辑:如何减少 GPU \text{GPU} GPU的等待(空闲)时间
- GPU \text{GPU} GPU返回真实 u i ∗ u_i^{*} ui∗时 CPU \text{CPU} CPU就算好了预测 u i ∗ u_i^{*} ui∗的邻居
- 一定概率使得下一次迭代开始时, CPU \text{CPU} CPU可立即传送邻居信息而不必让 CPU \text{CPU} CPU等待
- 选取逻辑:如何使(真实 u i ∗ == u_i^{*}\text{==} ui∗==预测 u i ∗ u_i^{*} ui∗)的概率更大
- 预选:选取 N i ′ N_i^{\prime} Ni′中的最近邻节点 + + +当前 WorkList \text{WorkList} WorkList中第一个未放问节点
- 择优:比较二者离查询点距离 → \to →选取之一最优者
👍这一优化使召回率增加了 10 % 10\% 10%
2.3. \textbf{2.3. } 2.3. 第三阶段: 实现重排的微内核
1️⃣重排操作
- 数据准备:从 GPU \text{GPU} GPU内存获取所有已异步传输来的全精度邻居
- 线程映射:
- 线程块:为每个查询(也就是个重排)分配一个线程块
- 单线程:将每个全精度邻居分给一个线程,不同线程并行地进行距离计算
- 内核操作:
- 距离计算:并行计算查询点 ↔ \xleftrightarrow{} 全精度邻居的全精度 L 2 L_2 L2向量
- 归并排序:采用与第二阶段中相同的并行归并历程,对全精度向量排序
- 输出:报告前 k k k个最近邻
2️⃣补充说明 & \& &算法分析
- 重充排独立内核:相比搜索阶段的内核,重排操作更为计算密集,建立独立内核有利于线程块大小优化
- 优化效果:使召回率提升了 10 - 15 % 10\text{-}15\% 10-15%
- 复杂度分析: ∣ C ∣ |C| ∣C∣是查询候选节点最大数量, d d d是全精度向量维度
Item \textbf{Item} Item 含义 复杂度 Work \text{Work} Work 算法串行执行总耗时 O ( ( d ⋅ ∣ C ∣ + ∣ C ∣ ⋅ log 2 ( ∣ C ∣ ) ) ⋅ ρ ) O((d \cdot|C|+|C|\cdot \log ^2(|C|)) \cdot \rho) O((d⋅∣C∣+∣C∣⋅log2(∣C∣))⋅ρ) Span \text{Span} Span 算法最耗时串行步骤耗时 O ( d + log 2 ( ∣ C ∣ ) ) O\left(d+\log ^2(|C|)\right) O(d+log2(∣C∣))
3. BANG \textbf{3. BANG} 3. BANG的不同版本
BNAG \textbf{BNAG} BNAG版本 数据规模 优化/改进方法 效果 原版 BANG \text{BANG} BANG 大 NULL \text{NULL} NULL NULL \text{NULL} NULL IM(In Memory)-BANG \text{IM(In Memory)-BANG} IM(In Memory)-BANG 中 将图结构直接放入 GPU \text{GPU} GPU内存,消除 CPU ⇆ GPU \text{CPU}\leftrightarrows{}\text{GPU} CPU⇆GPU通信 吞吐量提高 50 % 50\% 50% ED(Exact Dis.)-BANG \text{ED(Exact Dis.)-BANG} ED(Exact Dis.)-BANG 小 进一步直接用精确距离,省去 PQ \text{PQ} PQ距离表构建 & \& &重排操作 召回率提高
PART \textbf{PART } PART Ⅲ: 实验验证与结论
1. \textbf{1. } 1. 实验设置
1.1. \textbf{1.1. } 1.1. 数据/查询集
1️⃣数据集的选取:
- 大型数据集:十亿级
数据集 描述 数据点数量 维度 分布 DEEP1B \text{DEEP1B} DEEP1B 十亿个图像嵌入,压缩为 96 \text{96} 96维 1,000,000,000 \text{1,000,000,000} 1,000,000,000 96 \text{96} 96 均匀 SIFT1B \text{SIFT1B} SIFT1B 十亿个图像的 128 \text{128} 128维 SIFT \text{SIFT} SIFT描述符 1,000,000,000 \text{1,000,000,000} 1,000,000,000 128 \text{128} 128 均匀 SPACEV1B \text{SPACEV1B} SPACEV1B 来自 Bing \text{Bing} Bing的网页文档查询编码,使用 Microsoft SpaceV \text{Microsoft SpaceV} Microsoft SpaceV模型 1,000,000,000 \text{1,000,000,000} 1,000,000,000 100 \text{100} 100 均匀 - 中型数据集:亿级
数据集 描述 数据点数量 维度 分布 DEEP100M \text{DEEP100M} DEEP100M 从 DEEP1B \text{DEEP1B} DEEP1B中提取的前一亿个点 100,000,000 \text{100,000,000} 100,000,000 128 \text{128} 128 均匀 SIFT100M \text{SIFT100M} SIFT100M 从 SIFT1B \text{SIFT1B} SIFT1B中提取的前一亿个点 100,000,000 \text{100,000,000} 100,000,000 128 \text{128} 128 均匀 - 小型数据集:百万级
数据集 描述 数据点数量 维度 分布 MNIST8M \text{MNIST8M} MNIST8M 784 \text{784} 784维的手写数字图像数据集,包含变形和平移后的嵌入 8,100,000 \text{8,100,000} 8,100,000 784 \text{784} 784 均匀 GIST1M \text{GIST1M} GIST1M 一百万个图像的 960 \text{960} 960维 GIST \text{GIST} GIST描述符 1,000,000 \text{1,000,000} 1,000,000 960 \text{960} 960 均匀 GloVe200 \text{GloVe200} GloVe200 包含 1,183,514 \text{1,183,514} 1,183,514个 200 \text{200} 200维的词嵌入 1,183,514 \text{1,183,514} 1,183,514 200 \text{200} 200 不均匀 NYTimes \text{NYTimes} NYTimes 包含 289,761 \text{289,761} 289,761个 256 \text{256} 256维的词嵌入 289,761 \text{289,761} 289,761 256 \text{256} 256 不均匀 2️⃣查询集的设置:
- 默认设置:从数据集中随机选取 10000 \text{10000} 10000查询点
- 特殊设置:对 SPACEV1B \text{SPACEV1B} SPACEV1B选取前 10000 \text{10000} 10000点,对 GIST1M \text{GIST1M} GIST1M每次选取 1000 \text{1000} 1000个重复 10 \text{10} 10次
1.2. \textbf{1.2.} 1.2. 其它配置
1️⃣机器配置
- 硬件: Intel Xeon Gold 6326 \text{Intel Xeon Gold 6326} Intel Xeon Gold 6326处理器, NVIDIA Ampere A100 \text{NVIDIA Ampere A100} NVIDIA Ampere A100显卡(单个)
- 软件: Ubuntu 22.04.01 \text{Ubuntu 22.04.01} Ubuntu 22.04.01系统, g++ 11.3 \text{g++ 11.3} g++ 11.3编译器, nvcc 11.8 \text{nvcc 11.8} nvcc 11.8编译器( GPU \text{GPU} GPU)
3️⃣ BANG \text{BANG} BANG的参数设置
阶段 参数 DiskANN \text{DiskANN} DiskANN索引 最大定点数 R = 64 R\text{=}64 R=64,工作列表大小 L = 200 L\text{=}200 L=200,剪枝参数 α = 1.2 \alpha\text{=}1.2 α=1.2 PQ \text{PQ} PQ压缩 子空间数量 m = 74 m\text{=}74 m=74(试验确定) 搜索循环 查询批次大小 ρ = 10000 \rho\text{=}10000 ρ=10000, WorkList \text{WorkList} WorkList大小(结果启发式方法调整为 152 \text{152} 152) 4️⃣对比方法:
- 对比基准: GGNN/SONG/FAISS \text{GGNN/SONG/FAISS} GGNN/SONG/FAISS,所有参数均采纳原论文中最优参数
- 评价指标: k -recall@ k k\text{-recall@}k k-recall@k召回率, QPS \text{QPS} QPS吞吐量( Queries Per Second \text{Queries Per Second} Queries Per Second)
2. \textbf{2. } 2. 实验结果
2.2. \textbf{2.2. } 2.2. 在不同级别数据集上的表现
数据集 相同召回率下的 QPS \text{QPS} QPS 十亿级 BANG >FAISS>GGNN \textcolor{red}{\text{BANG}}\text{>FAISS>GGNN} BANG>FAISS>GGNN 亿级 ED-BANG > IM-BANG ≈GGNN> BANG \textcolor{red}{\text{ED-BANG}}\text{>}\textcolor{red}{\text{IM-BANG}}\text{}\text{≈}\text{GGNN}\text{>}\textcolor{red}{\text{BANG}} ED-BANG>IM-BANG≈GGNN>BANG 百万级 GGNN> ED-BANG > IM-BANG > BANG >SONG \text{GGNN}\text{>}\textcolor{red}{\text{ED-BANG}}\text{>}\textcolor{red}{\text{IM-BANG}}\text{>}\textcolor{red}{\text{BANG}}\text{>SONG} GGNN>ED-BANG>IM-BANG>BANG>SONG 2.3. \textbf{2.3. } 2.3. 其它结果
1️⃣ PQ \text{PQ} PQ压缩比对召回率的影响
- 关于压缩比:即压缩后数据与原数据的大小比,子空间数 M M M越少压缩比就越小
- 实验结果:可见 M = 74 → 32 M\text{=}74\text{→}32 M=74→32是召回率和压缩比 Trade-Off \text{Trade-Off} Trade-Off的最佳区间
M \textbf{M} M 压缩比 召回率 74 74 74 0.57 0.57 0.57 最高 74 → 32 74\text{→}32 74→32 0.57 → 0.25 0.57\text{→}0.25 0.57→0.25 缓慢下降 <32 \text{<32} <32 <0.25 \text{<0.25} <0.25 快速下降 2️⃣算法(主循环)迭代次数的研究
- 查询迭代次数的理论下界:不可少于工作列表 L \mathcal{L} L的长度,源于 L \mathcal{L} L中每点都必被处理一次
- 查询迭代次数的实验下界: 95 % 95 \% 95%的查询在 1.1 L 1.1 \mathcal{L} 1.1L次迭代内完成
3. \textbf{3. } 3. 结论
1️⃣在十亿数据集: BANG \text{BANG} BANG在维持超大吞吐量时,依旧保持 Recall \text{Recall} Recall较高不变
2️⃣在百万数据集: BANG \text{BANG} BANG在大多情况下,优于现有最先进算法