GitHub · 项目涌现

RyanCodrai/turbovec

二〇二六年八月二十六日·★ 7,633·⑂ 734·Python·MIT · GitHub 原仓库

turbovec是一个基于Google Research TurboQuant算法的Rust向量索引,提供Python绑定。它将1000万文档的float32存储从31 GB压缩至4 GB,搜索速度超过FAISS。该索引支持在线摄入、搜索时过滤和纯本地部署,无需训练或参数调优。在ARM上比FAISS IndexPQFastScan快12–20%,在x86上持平或更优。支持LangChain、LlamaIndex、Haystack和Agno框架集成。


一个包含 1000 万文档的语料库,以 float32 格式存储需要 31 GB 内存。turbovec 将其压缩至 4 GB,并且搜索速度比 FAISS 更快。

turbovec 是一个带有 Python 绑定的 Rust 向量索引,基于 Google Research 的 TurboQuant 算法构建——这是一种数据无关的量化器,其失真度接近香农下界,无需码本训练,也无需独立的训练阶段。

正在构建对隐私、内存或延迟敏感的 RAG?你来对地方了。

Python

pip install turbovec
from turbovec import TurboQuantIndex

index = TurboQuantIndex(dim=1536, bit_width=4)
index.add(vectors)
index.add(more_vectors)

scores, indices = index.search(query, k=10)

index.write("my_index.tq")
loaded = TurboQuantIndex.load("my_index.tq")

需要能在删除后保持稳定的 id?使用 IdMapIndex

import numpy as np
from turbovec import IdMapIndex

index = IdMapIndex(dim=1536, bit_width=4)
index.add_with_ids(vectors, np.array([1001, 1002, 1003], dtype=np.uint64))

scores, ids = index.search(query, k=10)   # ids 是你的 uint64 外部 id
index.remove(1002)                         # O(1) 按 id 删除

index.write("my_index.tvim")
loaded = IdMapIndex.load("my_index.tvim")

混合检索(带过滤的搜索)

将结果限制在由其他系统(SQL、BM25、ACL、时间窗口等)产生的候选集内:

import numpy as np
from turbovec import IdMapIndex

idx = IdMapIndex(dim=1536, bit_width=4)
idx.add_with_ids(vectors, ids)

# 阶段 1:外部系统将范围缩小到候选 id。
allowed = np.array(db.execute("SELECT id FROM docs WHERE tenant=?", (t,)).fetchall(),
                   dtype=np.uint64)

# 阶段 2:在候选集内进行密集重排序。
scores, ids = idx.search(query, k=10, allowlist=allowed)

过滤在 SIMD 内核内部以 32 向量块为粒度进行:没有允许槽位的块会在任何 LUT 查找或评分工作之前被短路处理,而已评分块中单个不允许的槽位会在堆插入时被丢弃。因此,选择性的允许列表(仅允许索引的一小部分)可以避免大部分 SIMD 开销,而不是先付出代价再丢弃结果。

输出长度是 min(k, len(allowed))——当允许列表小于 k 时,你会得到恰好 len(allowed) 个结果,而不是填充的备选结果。

完整参考请参见 docs/api.md

框架集成

可作为每个框架内置参考向量/文档存储的即插即用替代品。相同的公共接口,相同的持久化语义,相同的检索器和管道连接——只需替换 import 并保持你的管道不变。

Rust

cargo add turbovec
use turbovec::TurboQuantIndex;

let mut index = TurboQuantIndex::new(1536, 4);
index.add(&vectors);
let results = index.search(&queries, 10);
index.write("index.tv").unwrap();
let loaded = TurboQuantIndex::load("index.tv").unwrap();

对于能在删除后保持稳定的外部 id:

use turbovec::IdMapIndex;

let mut index = IdMapIndex::new(1536, 4);
index.add_with_ids(&vectors, &[1001, 1002, 1003]);
let (scores, ids) = index.search(&queries, 10);
index.remove(1002);
index.write("index.tvim").unwrap();
let loaded = IdMapIndex::load("index.tvim").unwrap();

召回率

TurboQuant vs FAISS IndexPQ (LUT256, nbits=8) — 论文第 4.4 节的基线。100K 向量,k=64。FAISS PQ 子量化器数量与 TurboQuant 的比特率匹配(2-bit 时 m=d/4,4-bit 时 m=d/2)。

Recall GloVe d=200

Recall d=1536

Recall d=3072

在 OpenAI d=1536 和 d=3072 上,TurboQuant 在 2-bit 和 4-bit 下的 R@1 比 FAISS 高 0.4–3.4 个百分点,两者在 k=4 时都收敛到 1.0。GloVe d=200 是更困难的场景——在低维度下,渐近 Beta 假设的约束较弱。TurboQuant 在 4-bit 下的 R@1 比 FAISS 高 0.3 个百分点,在 2-bit 下低 1.2 个百分点,两者在 k≈16 时都接近 FAISS。

关于基线的说明。 我们与 FAISS IndexPQ (LUT256, nbits=8, float32 LUT) 进行比较,因为它是大多数用户会使用的默认生产级 PQ。这比 TurboQuant 论文 中的自定义 u8-LUT PQ 基线更强——FAISS 在评分时使用更高精度的 LUT,并使用 k-means++ 进行码本训练。我们在 OpenAI d=1536 / d=3072 上复现了论文的 TurboQuant 数值,并在低维 embedding 上达到了与其他社区参考实现相似的数值(参见 d=384 的 turboquant-py)。在 GloVe 上可见的差距反映了 FAISS 是一个强基线,而不是 TurboQuant 实现的问题。

完整结果:d=1536 2-bit, d=1536 4-bit, d=3072 2-bit, d=3072 4-bit, GloVe 2-bit, GloVe 4-bit.

压缩率

Compression

搜索速度

所有基准测试:100K 向量,1K 查询,k=64,5 次运行的中位数。

ARM (Apple M3 Max)

ARM Speed — Single-threaded

ARM Speed — Multi-threaded

在 ARM 上,TurboQuant 在所有配置下都比 FAISS FastScan 快 12–20%。

x86 (Intel Xeon Platinum 8481C / Sapphire Rapids, 8 vCPUs)

x86 Speed — Single-threaded

x86 Speed — Multi-threaded

在 x86 上,TurboQuant 在所有 4-bit 配置下胜出 1–6%,在 2-bit 单线程下与 FAISS 的差距在 ~1% 以内。2-bit 多线程行(d=1536 和 d=3072)是唯一略落后于 FAISS(2–4%)的配置,其内部累加循环太短,无法通过展开摊销来匹配 FAISS 的 AVX-512 VBMI 路径。

工作原理

每个向量都是高维超球面上的一个方向。TurboQuant 通过一个简单的洞察来压缩这些方向:在应用随机旋转后,每个坐标都遵循一个已知的分布——无论输入数据如何。

1. 归一化。 从每个向量中剥离长度(范数),并将其存储为一个 float。现在每个向量都是超球面上的一个单位方向。

2. 随机旋转。 将所有向量乘以同一个随机正交矩阵。旋转后,每个坐标独立地遵循一个 Beta 分布,该分布在高维下收敛于高斯分布 N(0, 1/d)。这对任何输入数据都成立——旋转使坐标分布变得可预测。

3. 逐坐标校准 (TQ+)。 步骤 2 中的 Beta 分布是渐近的——在有限维度下,单个坐标会偏离标准形状(尤其是低位宽和词向量风格的 embedding)。TQ+ 在首次 add 期间为每个坐标拟合两个标量——一个移位和一个缩放——将每个坐标的经验 5/95% 分位数映射到标准 Beta 边缘分布上。然后,Lloyd-Max 码本针对其设计的目标分布进行量化。校准在首次 add 后冻结,并被后续的 add 重用——无需重新训练,无需重建,无需独立的训练阶段。召回率提升:在最偏离的单元(例如 2-bit 下的 GloVe)上,@1 最多提升 +1.4 个百分点。

4. Lloyd-Max 标量量化。 由于分布已知,我们可以预先计算每个坐标的最优分桶方式。对于 2-bit,有 4 个桶;对于 4-bit,有 16 个桶。Lloyd-Max 算法 找到能最小化均方误差的桶边界和质心。这些是根据数学计算得出的,而非从数据中学习。

5. 位打包。 每个坐标现在是一个小整数(2-bit 为 0-3,4-bit 为 0-15)。将这些整数紧密打包成字节。一个 1536 维的向量从 6,144 字节(FP32)压缩到 384 字节(2-bit)。这是 16 倍的压缩率。

6. 长度重归一化评分。 标量量化会系统性地低估内积——重建的单位方向比原始方向稍短。我们在编码时为每个向量计算一个标量——旋转后的单位向量与其自身质心重建的内积——并将 ||v|| / ⟨u, x̂⟩ 与每个压缩向量一起存储。搜索内核在堆插入之前将每个候选的分数乘以这个标量,将内积估计器从有向下偏差转变为无偏,且无需额外的搜索时间成本和存储空间。召回率的提升在低位宽时最为明显,因为此时量化收缩最大。

编码成本:每个向量额外进行一次 d 维点积来计算 ⟨u, x̂⟩。对于 100 万 d=1536 的向量,这不到一秒的额外编码时间——这是在摄入时一次性支付的代价,而非查询时。

搜索。 我们不是解压每个数据库向量,而是将查询旋转一次到同一域中,并直接根据码本值进行评分。评分内核使用 SIMD 内联函数(ARM 上的 NEON,现代 x86 上的 AVX-512BW,并带有 AVX2 回退),配合半字节分割查找表以实现最大吞吐量。

Lloyd-Max 码本的失真度在信息论下界(香农失真率极限)的 2.7 倍以内;长度重归一化步骤消除了 Lloyd-Max 码本在内积估计器上引入的残余偏差。

构建

Python (通过 maturin)

pip install maturin
cd turbovec-python
maturin build --release
pip install target/wheels/*.whl

Rust

cargo build --release

所有 x86_64 构建通过 .cargo/config.tomlx86-64-v3(AVX2 基线,Haswell 2013+)为目标。任何能运行 AVX2 回退内核的 CPU 都能运行整个 crate——AVX-512 内核在运行时通过 is_x86_feature_detected! 进行门控,仅在支持的硬件上启用。

运行基准测试

下载数据集:

python3 benchmarks/download_data.py all            # 所有数据集
python3 benchmarks/download_data.py glove          # GloVe d=200
python3 benchmarks/download_data.py openai-1536    # OpenAI DBpedia d=1536
python3 benchmarks/download_data.py openai-3072    # OpenAI DBpedia d=3072

每个基准测试都是 benchmarks/suite/ 中的一个独立脚本。单独运行任何一个:

python3 benchmarks/suite/speed_d1536_2bit_arm_mt.py
python3 benchmarks/suite/recall_d1536_2bit.py
python3 benchmarks/suite/compression.py

运行某个类别的所有基准测试:

for f in benchmarks/suite/speed_*arm*.py; do python3 "$f"; done    # 所有 ARM 速度测试
for f in benchmarks/suite/speed_*x86*.py; do python3 "$f"; done    # 所有 x86 速度测试
for f in benchmarks/suite/recall_*.py; do python3 "$f"; done       # 所有召回率测试
python3 benchmarks/suite/compression.py                            # 压缩率测试

结果以 JSON 格式保存到 benchmarks/results/。重新生成图表:

python3 benchmarks/create_diagrams.py

参考文献

同时见于 gh-search:rag、OSSInsight 全局趋势
译自 GitHub · 项目涌现 · 录于 二〇二六年八月二十六日