Elasticsearch DiskBBQ:使用原生 SIMD Blocks 实现快 40% 的向量评分计算

作者:来自 Elastic Benjamin Trent

深入解析 DiskBBQ 的块布局、文档 ID 压缩模式以及原生 SIMD 内核如何协同工作,为 9.4 版本中的 DiskBBQ 带来 40% 的向量评分吞吐量提升。

通过这个面向 Search AI 的**自助式动手实验** 亲自体验向量搜索。你现在就可以开始**免费的云试用** ,或者在你的**本地机器**上体验 Elastic。


Elasticsearch DiskBBQ 已经通过基于块的布局加速了 Better Binary Quantization (BBQ) 向量操作。Elasticsearch 9.4 新增了**原生单指令多数据(SIMD)内核**,使吞吐量相比单向量操作提升接近 3 倍。下面我们来看看这种格式是如何工作的。

DiskBBQ 通过密集且连续的块布局,直接从内存映射(memory-mapped)的索引文件中读取向量字节。这种方式能够保持较低的堆内存使用量,同时让引擎能够处理远超可用 RAM 容量的数据集。在这些块布局内部,DiskBBQ 还会对文档 ID 块进行编码,在减少磁盘占用的同时保持较低的解码开销。

本文将介绍其倒排列表(posting list)布局、文档 ID 打包模式,以及批量评分(bulk scoring)为何能够如此高效。

DiskBBQ 如何在磁盘上存储向量倒排列表

在每个倒排列表的开头,我们会写入质心(centroid)评分校正值、向量数量以及整体的文档 ID 编码方式。随后,我们以块(block)的形式写入文档 ID 和向量数据。

这些块及其内部数据都按照文档 ID 升序排列。这种设计遵循了分片(segment)文档 ID 的顺序,从而保留了索引排序(index sort order。与这种顺序对齐的过滤器更有可能一次性包含或排除整个数据块。

在存储完文档 ID 后,我们会存储量化后的向量字节数据,然后再存储该数据块中所有向量对应的校正值(corrections)。
倒排列表的布局。从元数据头(metadata header)开始,后面跟随多个数据块(block)。每个数据块包含 bulk_size 个文档 ID,然后是量化后的向量字节数据,最后是量化后的校正值(correction values)。

DiskBBQ 如何压缩文档 ID 以减少磁盘空间

为了确保文档 ID 数据块能够快速解码,DiskBBQ 会使用相同的编码格式对倒排列表中的每个数据块进行编码。

DiskBBQ 会先计算每个数据块所需的编码方式,然后选择整个倒排列表中空间开销最大的那种编码方式,并将其统一应用到所有文档数据块上。这样做虽然可能会牺牲部分压缩率,但能够简化解码逻辑并提高读取性能。

在撰写本文时,DiskBBQ 提供五种用于存储文档 ID 的压缩方案。每种方案都会先对文档 ID 值进行完整的增量编码(delta encoding,然后再在增量编码后的文档 ID 之上应用以下某一种编码类型。

编码类型 条件 节省的字节数(示例)
连续(Continuous) ID 连续递增(max−min+1 == len) 16 字节 → 4 字节
16 位增量编码(Delta 16) 所有增量值都能用 16 位表示 64 字节 → 33 字节
每个值使用 21 位 所有值都能用 21 位表示 12 字节 → 8 字节
每个值使用 24 位 所有值都能用 24 位表示 64 字节 → 48 字节
回退模式(Fallback,全量整数) 其他任何情况 无压缩效果

最高效的方式是连续编码(Continuous)。

当满足条件 max(doc_block) - min(doc_block) + 1 == len(doc_block) 时,就会使用这种编码方式。这意味着文档 ID 是连续递增的,因此增量编码(delta encoding)只需要存储最小值,后续的文档 ID 都可以通过在前一个值的基础上依次加 1 重新构造出来。

例如,文档 ID 为:

[4858192, 4858193, 4858194, 4858195]

在这种情况下,无需写入四个独立的 int 值(共 16 字节),只需要写入一个值:

4858192

解码时即可恢复为:

4858192, 4858193, 4858194, 4858195

因此,存储空间从 16 字节减少到 4 字节。
连续编码(Continuous encoding);只需要写入一个整数。

接下来是 delta 16

当所有 delta 都能放入 16 位(即两个字节)时,就会使用这种编码方式。

假设我们有:

doc_ids = [1000, 1003, 1010, 1020, 1041, 1055, 1070, 1090, 1100, 1125, 1200, 1300, 2000, 4000, 16000, 66000]

那么:

min = 1000

得到的 deltas 为:

deltas = [0, 3, 10, 20, 41, 55, 70, 90, 100, 125, 200, 300, 1000, 3000, 15000, 65000]

这些 16 个 delta 可以被打包成 8 个 int32(32 字节),再加上一个 min 值,从而使字节占用几乎减少 2 倍。
Delta 16;写入最小值,然后将两个 16 位的 delta 打包进每个 32 位整数中。

下一种是每个值 21 位编码。

这是一种相对复杂的方案,其中每组三个值会被压缩进一个 64 位的数值中,并且如果有剩余值,则会以 3 字节的尾部进行存储。

一个具体例子是:

doc_ids=[1000, 70000, 140000]

它们会被压缩成一个 64 位整数:

doc0 | (doc1 << 21) | (doc2 << 42)

最终结果是,三个原始整数值(共 12 字节)被压缩为 8 字节。
每个值 21 位,一种相当复杂的方案,将三元组压缩为 64 位值,并在末尾附加 3 字节的尾部。

倒数第二种压缩方式用于每个整数最多只需要 24 位的情况。

由于一个 int 通常需要 32 位空间,而 24 位会空出整整 1 个字节,因此不能浪费这部分空间。该方案的目标是尽可能填满所有字节。

因此,这种编码方式会把这些"空出来的字节"重新利用起来,把后续的数值填充进去。

例如:

docs_ids = [1000, 70000, 140000, 300000, 500000, 800000, 1000000, 1300000, 1600000, 1900000, 2200000, 3000000, 4000000, 8000000, 12000000, 16000000]

最终,最后四个整数会被压缩进前面整数留下的"空闲字节"中,从而把 16 个整数(通常需要 64 字节)压缩为 48 字节。
这是一个 24 位每值编码的示例,其中最后一组字节被打包进前面的空间中。它利用了那一"空闲字节"的存储空间。

最终一种是回退(fallback)方案。

DiskBBQ 会将每个 doc ID 以完整精度的整数形式存储,不进行任何磁盘压缩。因此,这种方式不会带来任何磁盘空间上的节省。由于 doc ID 在压缩前已经经过 delta 编码,因此 fallback 的使用频率非常低。

为什么 DiskBBQ 的批量评分更快:SIMD 与 CPU 缓存饱和

将向量按块存储使得可以进行 SIMD 优化的批量评分。这种方式能够让 CPU cache line 保持高利用率,从而持续加载向量字节,并使用 SIMD 优化内核来应用量化(quantization)校正值。

如果向量与校正值是交错存储的,那么在每次计算完一个向量分数之后,都需要立即应用校正,这会破坏吞吐量,并失去对校正应用进行优化的机会。

下面是一个性能基准测试(benchmark),展示了在不同优化下吞吐量的提升情况。这是一个在 M1Max MacBook 上使用 Java 25 运行的 JMH benchmark。向量维度为 1024,每次操作包含 10 个查询,在 10 个包含 32 个向量的 block 上执行。

复制代码
Benchmark           (bits)  (dims)  (similarityFunction)   Mode  Cnt   Score   Error   Units
Float32Scalar            1    1024           DOT_PRODUCT  thrpt    5   0.348 ± 0.014  ops/ms
Float32PanamaVector      1    1024           DOT_PRODUCT  thrpt    5   1.106 ± 0.043  ops/ms
BBQIndividual            1    1024           DOT_PRODUCT  thrpt    5   8.420 ± 0.180  ops/ms
BBQBulkPartial           1    1024           DOT_PRODUCT  thrpt    5  15.306 ± 0.757  ops/ms
BBQBULK                  1    1024           DOT_PRODUCT  thrpt    5  16.672 ± 0.572  ops/ms
BBQBulkNative            1    1024           DOT_PRODUCT  thrpt    5  23.258 ± 2.273  ops/ms

下面是对上述各个 benchmark 的说明:

  • Float32Scalar:纯 JVM 执行浮点运算,没有任何手工优化的 SIMD。

  • Float32PanamaVector:使用了部分 SIMD 优化的代码路径,基于 Panama Vector API。

  • BBQIndividual:逐个执行 bit-wise BBQ 操作。每个向量单独加载到 JVM heap 中进行评分和校正。

  • BBQBulkPartial :基于 mmap 文件段的 off-heap 批量评分,使用 Panama Vector操作读取数据;校正值随后在 JVM heap 上应用。

  • BBQBULK:完整的 off-heap 批量评分,向量和校正值都通过 SIMD 优化的 Java Vector API 从 mmap 文件中直接读取并处理。

  • BBQBulkNative:这是 Elasticsearch 9.4 中的实现。完全原生的批量向量操作,直接从索引文件读取字节并执行 SIMD 处理。

这些结果展示了吞吐量的演进过程,从最基础的逐个浮点运算开始。

切换到 SIMD(使用 Vector API 手动优化)后,浮点运算吞吐量提升约 3 倍,但仍然慢于 BBQ 中自动向量化的逐位运算方式。

随后切换到批量评分(bulk scoring)后,BBQ 吞吐量再次提升近 2 倍

再加入 Elasticsearch 9.4 的新优化原生 SIMD 内核后,又带来一次显著提升,使整体性能相比逐个 bit-wise 评分提升接近 3 倍 ,相较 float32 操作则达到了惊人的 66 倍提升
这展示了典型的批量评分路径。查询向量会先为块中的每个向量计算初始分数信息,然后再使用优化的 SIMD 块操作应用每个校正值。

这是典型的单向量评分路径。每个向量都会先被计算得分,然后再应用其校正值。这意味着向量字节无法充分填满 CPU cache,同时校正的应用也无法使用相同的 SIMD 块操作来执行。

DiskBBQ 向量搜索性能:原生 SIMD 在实践中的意义

Elasticsearch 9.4 中的原生 SIMD 内核使 BBQ 向量评分速度提升近 3 倍,并且吞吐量达到 float32 操作的 66 倍。我们还没有完成优化;总还有改进空间,但这种原生 SIMD 的块评分模式在所有 CPU 架构上都表现良好,使 DiskBBQ 在任何部署环境中都能保持高速。

这段内容有多大帮助?

原文:Elasticsearch DiskBBQ: 40% faster vector search with native SIMD - Elasticsearch Labs

相关推荐
Axis tech1 小时前
爱迪斯通携手智元机器人亮相COMPUTEX 2026大会
人工智能·机器人
AI刀刀1 小时前
智谱清言保存 pdf 显示该页的尺寸超出范围,AI 导出鸭智能适配页面尺寸稳定导出 PDF
人工智能·pdf·ai导出鸭
程序员佳佳2 小时前
连续使用三个月向量 API 中转站,它真的适配向量落地场景吗?
人工智能·gpt·aigc·ai编程·agi
男孩李2 小时前
浅谈open jiuwen
人工智能·ai
暴躁小师兄数据学院2 小时前
【AI大数据工程师特训笔记】第16讲:大数据环境安装
大数据·hadoop·笔记·flink·spark·database
豆豆2 小时前
垂直行业门户网站搭建解决方案与落地实操指南
大数据·cms·pageadmin·自定义模型·垂直门户·行业建站·站群建设
冬奇Lab2 小时前
每日一个开源项目(第121篇):tiktoken - OpenAI 出品的极速 BPE 分词器
人工智能·开源·openai
冬奇Lab2 小时前
Agent 系列(12):Agent 评估框架——怎么知道你的 Agent 到底好不好
人工智能·agent