RyanCodrai/turbovec
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 算法构建——这是一种数据无关的量化器,其失真度接近香农下界,无需码本训练,也无需独立的训练阶段。
- 在线摄入。 添加向量,它们即被索引——无需训练步骤,无需参数调优,语料库增长时也无需重建。
- 比 FAISS 更快。 手写的 NEON(ARM)和 AVX-512BW(x86)内核在 ARM 上比 FAISS IndexPQFastScan 快 12–20%,在 x86 上与之持平或更优。
- 搜索时过滤。 向
search()传递一个 id 允许列表(或一个槽位 bitmask),内核会直接处理它。你总能从允许的集合中获得最多k个结果——无需过度获取,在选择性过滤下也不会损失召回率。 - 纯本地。 无需托管服务,数据不会离开你的机器或 VPC。与任何开源 embedding 模型配合,即可构建一个完全隔离的 RAG 堆栈。
正在构建对隐私、内存或延迟敏感的 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 并保持你的管道不变。
- LangChain —
pip install turbovec[langchain]· 替代langchain_core.vectorstores.InMemoryVectorStore - LlamaIndex —
pip install turbovec[llama-index]· 替代llama_index.core.vector_stores.SimpleVectorStore - Haystack —
pip install turbovec[haystack]· 替代haystack.document_stores.in_memory.InMemoryDocumentStore - Agno —
pip install turbovec[agno]· 替代agno.vectordb.lancedb.LanceDb
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.toml 以 x86-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
参考文献
- TurboQuant: Online Vector Quantization with Near-optimal Distortion Rate (ICLR 2026) —— 本实现所依据的论文
- RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound for Approximate Nearest Neighbor Search (SIGMOD 2024) —— 步骤 5 中采用的逐向量长度重归一化校正的来源
- FAISS Fast accumulation of PQ and AQ codes —— turbovec 的 x86 SIMD 内核借鉴了 FastScan 的打包布局、半字节 LUT 评分和 u16 累加器策略