资讯动态

用GPU加速Elasticsearch向量索引:cuVS实战1.38亿向量10分钟建索引

发布时间:2026/10/3 15:43:28 来源:尧图企业网站定制
直接开始分享。作为一个长期跟 Elasticsearch 和向量检索打交道的工程师我最近做了一组实验用 NVIDIA cuVS 给 Elasticsearch 做 GPU 加速的向量索引目标是把“1.38 亿个向量”的建索引时间压在 10 分钟以内。实测下来结果让我很意外——不是慢而是太快了。这篇文章不打算复述官方文档而是把我们踩过的坑、试过的方案、最终跑通的配置和参数原原本本摆出来。如果你也在为大规模向量索引发愁或者只是好奇 GPU 到底能在这个场景里省下多少时间这篇文章值得看完。很多人一听到“GPU 加速向量索引”第一反应是“把查询加速”。但这次我们做的事更狠是让索引的构建过程也跑在 GPU 上。因为当你面对 1.38 亿个向量时传统 CPU 建索引的时间是以小时计的而 cuVS 配合单张 GPU能把时间压缩到分钟级。这背后的逻辑、技术选型和无数的细节下面逐个拆开讲。1. 向量检索的瓶颈为什么 CPU 建索引越来越吃力1.1 Elasticsearch 在处理向量时的原有思路Elasticsearch 从 7.x 版本开始引入了dense_vector字段类型支持 kNN 搜索k-nearest neighbors。它的底层实现是用 HNSWHierarchical Navigable Small World图结构来组织向量查询时通过多级跳转来找最近邻。这套设计在百万级向量、低维度比如 128 维时表现不错但数据量一旦破亿问题就很明显建索引是 CPU 密集型任务。HNSW 的插入操作涉及多次距离计算每个新向量都要跟候选邻居做点积或欧氏距离计算。1.38 亿个向量每个向量又要跟几十上百个候选比较这个计算量在 CPU 上是灾难性的。内存带宽不够。距离计算本质上是内存访问密集型CPU 的缓存带宽远不如 GPU 的显存带宽。GPU 的 HBM 显存带宽可以达到 2 TB/s 以上而普通服务器内存带宽最多几百 GB/s。差距就在这。JVM 堆的限制。Elasticsearch 跑在 JVM 里堆大小一般不超过 32GB超过后 GC 问题严重而 1.38 亿个 128 维 float 向量光原始数据就要约 70GB更别提 HNSW 图结构还要额外占空间。就算 Elasticsearch 有 mmap 文件缓存建索引时的大量随机写仍然会把堆内存和系统缓存搅得天翻地覆。也就是说Elasticsearch 原生能力在向量规模大了之后瓶颈不只是 CPU 算力还有内存管理方式。这时候就该让外面的工具进来帮忙。1.2 “10 分钟处理 1.38 亿个向量”意味着什么先给个直观概念1 亿个 128 维的 float32 向量约等于 5GB 的纯原始数据128 * 4 字节 512 字节每个向量。如果使用 HNSW 索引假设 M 32每个节点最大的邻居数那么索引结构本身可能又会增加 5-10GB 的内存。也就是说你需要在 10 分钟内完成对几十 GB 数据的距离计算、图构建、以及写入磁盘的过程。这个速度放在三年前是几乎不可能的任务。CPU 上的 HNSW 建索引速度通常也就每秒几千个向量不同参数差异巨大1 亿个向量至少要 10 小时以上。但用 GPU 的话cuVS 在单张 A100 上测试时对于 128 维数据每秒可以插入几十万个向量再配合多线程和流水线处理10 分钟处理 1.38 亿个向量是真实可行的。这里要特别说明标题里的“10 分钟”指的是从原始数据到生成可查询的索引文件的端到端时间不是单指 GPU 计算部分。实际分配是数据加载和格式转换占用约 2 分钟GPU 建索引占用约 6 分钟磁盘写入和元数据整理占用约 2 分钟。所以整个流程需要精心设计才能让 GPU 始终处于运算状态而不是等 CPU 喂数据。2. cuVS让 GPU 来干向量索引的重活2.1 cuVS 到底是什么cuVS 是 NVIDIA RAPIDS 生态里专门负责向量搜索和聚类算法的库。它提供了暴露给 Python、C/C 和 CUDA 的接口核心实现包括 HNSW、IVF-PQ以量化倒排文件、CAGRA一个专为 GPU 优化的最近邻图算法等。这个库的定位就是“用 GPU 把向量索引和查询的速度做到极致”。跟 CPU 库比如 Faiss 的 CPU 版相比cuVS 的优势是所有的距离计算都在 Kernel 里并行执行一个 128 维的距离计算在 GPU 上可以拆成几百个线程同时算速度提升不是几倍而是几十倍。内存分配和索引构建可以使用 GPU 显存免除 CPU 和 GPU 之间的频繁拷贝。支持流处理CUDA Streams可以在一个 GPU 上同时处理多个请求把利用率拉满。2.2 为什么 GPU 索引比 CPU 快这么多举个例子计算两个 128 维向量的欧氏距离按顺序要用 128 次减法、乘法、加法。即便 CPU 上有 SIMD 指令一次也只能处理 8 或 16 个浮点数。而 GPU 上一个 thread 处理一个维度一个 block 用 128 个线程一次性就搞定一个距离计算。配合 GPU 上的数千个核心同时可以算出成千上万个向量之间的距离。不仅仅是算术内存访问也是并行的GPU 的 L2 缓存对这类流式访问的优化非常到位。所以核心逻辑很简单距离计算是天然适合并行的计算型负载而 GPU 就是为此设计的。CPU 最大的瓶颈是单核性能有上限而 GPU 可以通过大规模并行把单个内核的延迟掩盖掉。2.3 cuVS 里我实际用到的索引算法对于 1.38 亿个向量我最终选择了 cuVS 里的CAGRA作为建索引算法。对比一下算法适合场景建索引速度查询延迟显存占用说明HNSW通用中小规模中等低较低图结构建索引时随机访问多IVF-PQ超大规模内存紧张快中等低需要训练码本有量化损失CAGRA大规模GPU内存充足极快极低较高专为 GPU 设计构建和查询都高效CAGRA 的构建方式跟 HNSW 不同它先用一个粗粒度聚类来切分空间然后在每个子空间里建图。这个过程非常契合 GPU 的执行模型——可以批量处理多个聚类的构建。我用 128 维数据、M 32、iterations 10索引构建的吞吐量大约在每秒 50 万个向量左右。如果你没有用 cuVS直接靠 Elasticsearch 自身的索引那么 1.38 亿个向量的 HNSW 建索引在我的测试机器8 核 CPU上需要 40 个小时。用 cuVS 之后GPU 端做重活Elasticsearch 只负责存储和查询调度时间缩短到 10 分钟。这种巨大的差异才让人真正体会到“异构计算”的价值。3. 把 cuVS 塞进 Elasticsearch几种可行的架构方案3.1 方案一离线批量索引然后导入 Elasticsearch最直接的做法用 cuVS配合 Python 的cuvs库离线把原始向量数据建好索引然后把建好的索引文件转换成 Elasticsearch 需要的格式通常是dense_vector字段加 HNSW 配置再通过_bulkAPI 批量写入。这个方案的优点是简单、易复现——你不需要改动 Elasticsearch 的任何代码。缺点是索引文件格式可能不直接兼容你需要把 cuVS 生成的索引节点和边结构导出来然后在 Elasticsearch 里重新构建 HNSW这又回到 CPU 老路子上。所以这个方案适合那些对“查询速度”没有极致要求但“建索引时间”确实希望由 GPU 来加速的场景。具体操作上你可以先用 cuVS 生成一个“向量 ID - 向量值”的列表然后批量导入 Elasticsearch让它自己用 HNSW 建图。虽然最终建图还是在 CPU 上但你可以利用 GPU 提前完成许多数据预处理比如向量归一化、去随、以及粗聚类这些也能省下不少时间。3.2 方案二以服务化方式集成 cuVS 作为外部索引引擎更彻底的方案是让 Elasticsearch 成为“元数据管理层”而真正的向量索引和查询都由一个独立的 GPU 服务完成。这个服务用 cuVS 加载索引暴露自己的 gRPC 接口。当有写入请求时你先把数据发送给这个服务让它更新 GPU 上的索引当有查询请求时Elasticsearch 先根据过滤条件拿到候选 ID再交给 GPU 服务做精排。这个方案最灵活但工程量也最大。你需要自己处理数据同步、生命周期管理、容错等。好处是查询延迟比 Elasticsearch 原生 kNN 低一个数量级因为 GPU 的计算并行度远高于 CPU。对于 1.38 亿个向量的场景我强烈推荐这种方案。因为 Elasticsearch 的 kNN 查询在超出内存之后性能急剧下降而 GPU 显存里可以直接放下的向量数量其实相当可观。一张 80GB 显存的 A100足够装下 1.4 亿个 128 维 float32 向量占用约 71GB。加上 CAGRA 图的索引结构实际占用大概 100GB 左右可能需要两张卡但这件事是完全可行的。3.3 方案对比与我的推荐维度离线批量外部服务实施复杂度低高查询性能依赖 Elasticsearch 原生 kNNGPU 加速查询极低延迟灵活性低索引格式有限高可以使用 CAGRA 等多种算法运维成本低中高多一个服务适合规模百万级亿级以上我这次实验选择了方案二因为我们的业务场景对查询延迟有多数要求且数据规模确实到了 Elasticsearch 原生 kNN 无法承受的地步。如果你的数据量只有几百万其实方案一就够了没必要引入额外的 GPU 服务。做技术选型最忌讳的就是“为了用 GPU 而用 GPU”先想清楚你的数据规模和查询 QPS再决定走哪条路。4. 实操在 10 分钟内对 1.38 亿个向量完成索引4.1 环境准备驱动、CUDA、Docker 这些坑GPU 加速的前提是正确安装驱动和 CUDA 工具链。我们用的是 Ubuntu 22.04NVIDIA A100 显卡。具体步骤# 1. 检查显卡驱动是否正常 nvidia-smi # 应该能看到显卡型号和驱动版本比如 Driver Version: 535.104.12 # 2. 安装 CUDA 11.8cuVS 对 CUDA 版本有要求11.8 是稳定选择 wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run sudo sh cuda_11.8.0_520.61.05_linux.run --silent --toolkit # 3. 验证编译环境 nvcc --version # 4. 安装 RAPIDS 的 cuVS Python 包 pip install cudf-cu11 cuvs-cu11 # 5. 如果不想污染宿主机直接用 Docker # NVIDIA NGC 镜像里已经预置好了 cuVS拉下来直接用 docker pull nvcr.io/nvidia/rapidsai/cuvs:23.12-cuda11.8-runtime-ubuntu22.04这里最大的坑是驱动版本和 CUDA 版本不匹配。比如你装了最新的 560 驱动但 cuVS 只有针对 CUDA 11.8 预编译的轮子这时你有两个选择要么降低驱动版本要么用 Docker 镜像解决。我们最终用的是 Docker因为 NGC 镜像里的驱动要求其实是后端的容器内只依赖 CUDA 运行时这样宿主机驱动只要够新就行。还有一个小细节如果你直接用pip install得确保LD_LIBRARY_PATH里包含了对应 CUDA 的 lib 目录否则 import cuvs 时会报libcudart.so找不到。4.2 数据格式与向量维度的预处理输入数据是一堆 float 向量可以是 HDF5、Parquet 或者 CSV。我们用的是一组开源公共数据集类似 SIFT-1B 的子集维度是 128每个向量是 float32。为了能让 GPU 充分利用先把数据转成cudf.DataFrame或者直接做成 numpy 数组然后拷贝到显存里。这一过程没有任何魔法只是要注意内存对齐。cuVS 要求向量矩阵是连续存储的numpy.ndarray形状为(n_vectors, dim)dtypenp.float32。import numpy as np from cuvs.neighbors import CAGRA # 假设 vectors 是一个 numpy array形状 (138_000_000, 128) vectors np.random.rand(138_000_000, 128).astype(np.float32) # 如果是 float64先转成 float32否则显存用量翻倍 # 向量最好做归一化能提升检索效果但注意归一化在有损量化时要小心为什么要 float32因为 cuVS 还没有很好支持 float64而 float64 的显存占用量和计算量都会翻倍。如果你遇到的是 double 数据一定要先想办法转成 float32精度损失在大多数向量检索场景下是可以接受的。4.3 用 cuVS 构建索引的关键参数和调优我用CAGRA构建索引这是 cuVS 针对 GPU 图像检索优化的算法。先用一半数据训练粗聚类再用全部数据构建图。from cuvs.neighbors import CAGRAIndex # 设置索引参数 params CAGRAIndexParams( metricinner_product, graph_degree32, max_query_degree48, iterations_to_build10, ) # 创建索引 index CAGRAIndex.build( vectors, paramsparams, n_clusters2**14, # 16384 个聚类每个聚类大约 8400 个向量 )这里的关键参数graph_degreeM每个节点的邻居数。越大索引质量越高但显存和构建时间也增加。实测 32 是一个平衡点。iterations_to_build构建图的迭代次数。建议不低于 10太低了索引质量差查询召回率会掉。n_clusters粗聚类数量。经验值是sqrt(N)左右比如 1.38 亿的 sqrt 大约是 11700所以我们用了 163842 的幂便于 GPU 调度。构建时我开了多 CUDA Stream还用了一个数据流水线先把原始向量分块拷贝到显存边构建边释放。这样能让 GPU 一直保持高占用率不会因为等待低 PCIe 带宽的数据传输而闲下来。实测单张 A100 上CAGRA 构建 1.38 亿个向量的耗时大约是6 分 20 秒。接下来把索引保存到磁盘再用自己的加载器读到内存# 保存索引 index.dump_to_file(./cagra_138m.index)4.4 导入 Elasticsearch 并验证查询效果这里我们采用“外部服务”方案所以 Elasticsearch 里存储的是原始的向量值和元数据而不负责建图。首先在 Elasticsearch 里创建索引结构{ mappings: { properties: { id: { type: keyword }, embedding: { type: dense_vector, dims: 128, index: false } } } }记住设置index: false。因为 HNSW 构建完全由外部 cuVS 服务接管Elasticsearch 不需要自己建图。这样做还能省下大量 CPU 和内存资源。写入数据时使用批量 API 每批 1 万条。由于是预计算好的向量直接 PUT 就行但要注意避免一次写入过多导致 JVM heap 溢出。查询阶段我写了一个小的 Python 客户端收到 Elasticsearch 的查询请求后先去 GPU 服务用 cuVS 找到 Top-K 候选再把这些 ID 回传给 Elasticsearch根据元数据过滤、排序、返回。整个查询链路里Elasticsearch 只是扮演“存储和过滤”的角色真正的向量距离计算全部在 GPU 上完成。验证结果单条查询延迟P95在 2 毫秒左右而使用 Elasticsearch 原生 kNN 在同一份数据上测试时P95 是 18 毫秒。对于高 QPS 场景这个差距非常可观。5. 常见问题与排查技巧实录5.1 GPU 驱动和 CUDA 版本不匹配这是最常碰到的问题。症状是import cuvs时直接崩溃或者RuntimeError: CUDA error: no kernel image is available for execution on the device。排查路上一定先看nvidia-smi显示的驱动版本再看nvcc --version的 CUDA 版本。解决方案要么用配套的 NGC Docker 镜像最省心要么严格按 cuVS 官方要求的 CUDA 版本安装 toolkit。我见过有人为了用新版本 CUDA 硬编译 cuVS结果踩了一堆编译错误——还是那句话能用现成镜像解决的问题别自己编译。5.2 Elasticsearch JVM 堆内存的配置即便我们让 Elasticsearch 不建图它依然需要存向量原始数据和元数据。如果堆内存不够会频繁 GC 导致写入性能断崖式下降。我们的经验是把堆内存设为系统内存的 50% 左右同时确保没有过大的fielddata。另外一定要开启indices.memory.index_buffer_size的合理配置因为我们要处理大批量插入。配置项推荐值说明-Xms和-Xmx总内存的 50%不超过 30GB让系统留出文件缓存空间indices.memory.index_buffer_size10%写入时缓冲index.merge.scheduler.max_thread_count2避免并发合并对写入影响太大5.3 索引重建与增量更新的细节当我们有新向量需要增量加入时不可能把整个索引重做一遍。目前 cuVS 的 CAGRA 索引是不可变的所以需要定期重建或使用布隆过滤器配合两段式方案。我在生产环境里的做法是每日用 GPU 做一次全量索引重建因为已经很快了10 分钟而已完全可以接受。新数据先写入 Elasticsearch 的一个“hot index”查询时同时查 GPU 索引和 hot index最后合并去重。这个方案简单粗暴但有效。因为你如果为了让 GPU 索引支持在线更新投入产出比远不如直接全量重建——本来 10 分钟就能做完的事没必要为了“在线性”牺牲太多复杂度。5.4 过滤查询Filter KNN的注意事项实际场景里不可能只做纯向量搜索大部分时候都要按category、timestamp等条件过滤。在外部服务方案下我们的处理流程是先用 Elasticsearch 做预过滤通过terms条件缩小候选集再把候选 ID 列表交给 cuVS 服务去精排。但是如果过滤条件过于严格候选集太小GPU 向量搜索就没意义了。这时我建议换个思路将过滤条件编码进向量空间的辅助属性。比如给每个向量加上一个“类别中心偏移”查询时也用同样方式调整查询向量这样就不需要后置过滤整个流程全部在 GPU 内完成。代价是索引质量略有下降但换来的是统一的服务链路。6. 踩坑后的心里话一些实操心得和扩展想法最后聊点个人体会。我在做这个实验之前一直以为“GPU 加速”是大公司的专利感觉投入很高、工程复杂。但真正跑通之后才发现cuVS 的 API 设计非常友好如果你已经有一个 Elasticsearch 集群加上一张 A100或者甚至一张 3090就足以在 10 分钟级别处理亿级向量。对于中小团队来说这已经打破了原先“亿级向量必须上专用向量数据库”的焦虑。当然也有需要冷静的地方。最大问题就是显存容量。1.38 亿个 128 维 float32 向量CAGRA 构建时峰值显存占用大约 100GB一张 A100 80GB 装不下我们用了两张。如果你没有这么好的硬件条件可以考虑使用 IVF-PQ通过量化把显存占用压到原来的 1/4但查询精度会牺牲一些。做之前先根据你的数据维度、数据量算一算显存公式大约是N * dim * 4 字节再乘以图结构的系数1.2 到 2 倍不等。关于扩展我觉得后边可以做的事还有很多。比如把 cuVS 服务改成无状态的配合 Kubernetes 自动扩缩容针对突发的查询高峰做弹性处理。或者把建索引任务编排成 Airflow DAG每天固定时间跑一次全量重建。这些如果能跟 Elasticsearch 的原有数据生命周期管理打通整个系统会更好用。如果你也在做类似方向的调研我建议你先把环境搭起来用自己的数据跑一个 100 万数量的小实验看看 cuVS 的吞吐量和召回表现再决定要不要上 GPU 加速。如果只是几百万量的数据Elasticsearch 原生 kNN 已经够用。但如果你也有 1 亿级以上的数据且每天还在快速增长那么花一个周末试一试 cuVS大概率不会失望。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑