1000 萬筆文件語料庫使用 float32 格式需要 31 GB 的 RAM。TurboQuant 將其壓縮至 4 GB,並且搜尋速度比 FAISS 更快。

turbovec 是一個基於 Rust 的向量索引,提供 Python 綁定,它建構於 Google Research 的 TurboQuant 演算法之上——這是一種數據無關的量化器,具有近乎最佳的失真率且無需獨立訓練階段。

正在建構需要考量隱私、記憶體或延遲的 RAG 系統嗎?您來對地方了。

向量和查詢是形狀為 (n, dim) 的二維 float32 陣列——其他資料類型將被拒絕而不是被靜默轉換,因此如果需要,請先使用 np.asarray(x, dtype=np.float32) 進行轉換。

需要可應對刪除操作的穩定 ID 嗎?請使用 IdMapIndex:

將結果限制在由另一個系統(SQL、BM25、ACL、時間窗口等)產生的候選集中:

過濾操作發生在 SIMD 核心內部,以 32 個向量為一個區塊進行處理:沒有允許插槽的區塊會在任何 LUT 查找或評分工作之前被短路,並且在堆疊插入時,在已評分的區塊內不允許的個別插槽會被丟棄。因此,選擇性允許列表(僅允許索引的一小部分)可以避免大部分 SIMD 成本,而不是支付成本後再丟棄結果。

輸出長度為 min(k, n_allowed),其中 n_allowed 計算允許的獨立向量數量——當允許的向量少於 k 時,您將獲得確切的數量,而不是填充的備用結果。

請參閱 docs/api.md 以取得完整參考。

作為各框架中內建參考向量/文件儲存的直接替換品。相同的公開介面、相同的持久化語義、相同的檢索器和管道連接——只需交換匯入即可保留您的管道。

對於可應對刪除操作的穩定外部 ID:

TurboQuant 與 FAISS IndexPQ (LUT256, nbits=8) — 論文的第 4.4 節基準。10 萬個向量,k=64。FAISS PQ 子量化器計數的大小與 TurboQuant 的位元率相匹配(2 位元時 m=d/4,4 位元時 m=d/2)。

圖表繪製了校準後的 TurboQuant (TQ+)。在 OpenAI d=1536 和 d=3072 上,TQ+ 在四個單元格中的三個上以 R@1 的表現優於 FAISS(領先 0.9–2.9 個點;d=1536 的 4 位元落後 0.7),並且兩者在 k=8 時都達到 1.0(k≤4 時已達 ≥0.997)。GloVe d=200 是更困難的場景——在低維度時,漸近 Beta 假設較為寬鬆。TQ+ 在 4 位元和 2 位元下的 R@1 表現均優於 FAISS(+1.9 和 +0.8),而 FAISS 在 k≈8 時在 2 位元下保持微弱優勢。未校準的數字在 JSON 中(tq_recalls)。

關於基準的說明。我們將 FAISS IndexPQ (LUT256, nbits=8, float32 LUT) 作為基準,因為它是大多數用戶會選擇的預設生產級 PQ。這比 TurboQuant 論文中的自訂 u8-LUT PQ 更強的基準——FAISS 在評分時使用更高精度的 LUT,並使用 k-means++ 進行碼本訓練。我們重現了論文中關於 OpenAI d=1536 / d=3072 的 TurboQuant 數字,並在低維嵌入上達到了與其他社群參考實作相似的數字(請參閱 d=384 的 turboquant-py)。在 GloVe (d=200) 上——漸近 Beta 假設最寬鬆的低維場景——TurboQuant 在 4 位元下優於 FAISS,但在 2 位元下落後;TQ+ 校準在 R@1 上恢復了 2 位元的差距(0.572 對 FAISS 的 0.564),而 FAISS 在更深的 k 值下保持微弱優勢。

完整結果:d=1536 2 位元、d=1536 4 位元、d=3072 2 位元、d=3072 4 位元、GloVe 2 位元、GloVe 4 位元。

所有基準測試:10 萬個向量,1000 個查詢,k=64,5 次運行取中位數。

在 ARM 上,TurboQuant 在所有配置中都優於 FAISS FastScan,平均速度為 4 位元時的 3.5 倍(在各個單元格中為 3.4–3.7 倍——SDOT/SMMLA 點積核心直接對向量主佈局進行評分)和 2 位元時的 26%(22–29%)。

在 x86 上,TurboQuant 在所有配置中都獲勝,平均速度為 4 位元時的 3.4 倍(在各個單元格中為 3.2–3.5 倍——向量主佈局上的 AVX-512 VNNI 點積核心)和 2 位元時的 20%(5–32%),其中 vpermb LUT 掃描承載了短的 2 位元累加循環。

與搜尋單元格相同的語料庫:10 萬個 OpenAI 向量,5 次運行取中位數。插入操作測量的是預熱、已填充索引(已構建但未計時)上每向量 add() 的延遲,n=1——單向量 add(),以及 n=100——100 向量批次,顯示批次處理如何攤銷每次操作的開銷——與插入到已訓練、已填充的 FAISS IndexPQFastScan(訓練未計時)的比較。單次 add() 的延遲為 6.3–19.7 µs,取決於單元格(比 FAISS 單次 add() 快 7.6–13.9 倍),而 100 向量批次的 TurboQuant 攤銷為 4.6–16.3 µs/向量(比相同批次插入 FAISS 快 4.6–15.1 倍)。刪除操作測量的是 n=1 時每操作 remove-by-id 的延遲(1000 次刪除的穩定每操作速率)和 n=100(新索引上的前 100 次刪除):IdMapIndex.remove(id) — O(1) 的交換和彈出加上 ID 映射的簿記 — 在各個單元格中的延遲為 0.44–1.22 µs 和 0.59–1.37 µs/操作。FAISS 列顯示的是相同的用戶可見操作,即 IndexIDMap 上的 remove_ids,它在每次調用時重新打包儲存的代碼:在 10 萬個操作時,單次刪除的延遲為 0.19–1.02 秒,成本隨著代碼大小的增加而翻倍——這就是為什麼刪除圖表使用對數刻度軸的原因。圖表顯示的是單線程單元格(RAYON_NUM_THREADS=1);_mt 單元格也進行了測量,並且在 n=1 時結果相同,因為單次 add 是串行的。腳本:benchmarks/suite/。

完整結果:d=1536 2 位元插入、d=1536 4 位元插入、d=3072 2 位元插入、d=3072 4 位元插入,以及匹配的 speed_remove_* 和 _mt 文件。

完整結果:d=1536 2 位元插入、d=1536 4 位元插入、d=3072 2 位元插入、d=3072 4 位元插入,以及匹配的 speed_remove_* 和 _mt 文件。

與搜尋單元格相同的語料庫:10 萬個 OpenAI 向量,5 次運行取中位數。TurboQuant 序列化為單個 .tv 文件,並進行 fsync + 原子重命名;FAISS 則是在精確匹配的 IndexPQFastScan 上進行 write_index / read_index(子量化器計數與 TurboQuant 的位元率匹配,如搜尋單元格所示)。儲存(預熱)是在搜尋運行後進行寫入,因此塊狀佈局緩存已填充。載入 → 首次搜尋會打開一個新索引並計時首次查詢——將純粹的反序列化(頁緩存在此過程中保持預熱狀態,因此這是佈局工作,而不是冷儲存 I/O)與首次查詢成本分開。往返鏈接了嵌入儲存實際支付的檢查點/恢復週期——修改 1000 個向量 → 儲存 → 重新開啟 → 提供首次查詢;FAISS 沒有對此路徑進行測量的對應項,因此僅顯示 TurboQuant 的情況。在較小的負載下,往返可能會低於獨立的變更後(「髒」)寫入:兩者在單獨的套件步驟中計時,並且在文件大小較小時,髒寫入步驟中的獨立 fsync 會佔主導地位並膨脹它——這是硬體的一種測量偽影,而不是組合路徑中的重新打包優勢。單線程單元格固定 RAYON_NUM_THREADS=1。腳本:benchmarks/suite/。

完整結果:d=1536 2 位元持久化 ST、MT,d=1536 4 位元持久化 ST、MT,d=3072 2 位元持久化 ST、MT,d=3072 4 位元持久化 ST、MT。

完整結果:d=1536 2 位元持久化 ST、MT,d=1536 4 位元持久化 ST、MT,d=3072 2 位元持久化 ST、MT,d=3072 4 位元持久化 ST、MT。

每個向量都是高維超球面上的一個方向。TurboQuant 使用一個簡單的洞察來壓縮這些方向:在應用隨機旋轉後,每個坐標都遵循一個已知分佈——無論輸入數據如何。

1. 標準化。去除每個向量的長度(範數)並將其儲存為單個浮點數。現在每個向量都是超球面上的單位方向。

2. 隨機旋轉。將所有向量乘以相同的隨機正交矩陣。旋轉後,每個坐標獨立遵循一個 Beta 分佈,在高維度時收斂到高斯分佈 N(0, 1/d)。這對任何輸入數據都成立——旋轉使得坐標分佈可預測。

3. 每坐標校準 (TQ+)。步驟 2 中的 Beta 分佈是漸近的——在有限維度下,個別坐標會偏離標準形狀(特別是低位元和詞向量風格的嵌入)。TQ+ 為每個坐標擬合兩個標量——一個偏移量和一個縮放量——將每個坐標的經驗分位數映射到碼本的最外層質心。機率級別來自碼本,因此它跟隨位元寬度(2 位元時約 0.933,4 位元時約 0.996),而不是固定值。然後,Lloyd-Max 碼本會針對其設計的目標分佈進行量化。擬合是顯式的:在添加向量之前,使用隨機、代表性的向量樣本(約 1024 行就足夠了——這樣的大小相當於擬合整個語料庫)調用 index.calibrate(sample) 一次;之後,校準將被提交並由每次 add 操作重複使用——無需重新訓練、無需重建、無需獨立訓練階段。未校準的索引就是純粹的 TurboQuant。index.calibration_state 報告「uncalibrated」或「calibrated」。召回率增益:在最偏離的單元格上(例如,2 位元的 GloVe)的 @1 上最多可提高 +2.2 個百分點。

4. Lloyd-Max 純量量化。由於分佈是已知的,我們可以預先計算出每個坐標的最佳分組方式。對於 2 位元,有 4 個分組;對於 4 位元,有 16 個分組。Lloyd-Max 演算法找到最小化均方誤差的分組邊界和質心。這些是從數學上一次計算出來的,而不是從數據中計算出來的。

5. 位元打包。每個坐標現在是一個小整數(2 位元為 0-3;4 位元為 0-15)。將這些緊密地打包到字節中。一個 1536 維的向量從 6,144 字節(FP32)壓縮到 384 字節(2 位元)。這是 16 倍的壓縮。

6. 長度重新歸一化評分。純量量化系統性地低估了內積——重建的單位方向比原始向量稍短。我們在編碼時為每個向量計算一個標量——旋轉後的單位向量與其自身質心重建的內積——並將 ||v|| / ⟨u, x̂⟩ 與每個壓縮向量一起儲存。搜尋核心在堆疊插入之前,將每個候選分數乘以這個標量,將內積估計器從有偏差變為無偏差,且搜尋時間成本和額外儲存空間為零。召回率增益在低位元寬度下最為顯著,因為量化收縮最大。

編碼成本:每個向量額外進行一次 d 維點積計算 ⟨u, x̂⟩。對於 100 萬個 d=1536 的向量,這只需要不到一秒的額外編碼時間——這是攝取時一次性支付的成本,而不是查詢時的成本。

搜尋。與其解壓縮每個資料庫向量,不如將查詢一次旋轉到相同的域,並直接針對碼本值進行評分。評分核心使用 SIMD 指令集(ARM 上的 NEON;現代 x86 上的 AVX-512BW,回退到 AVX2,然後在 AVX2 之前的 CPU 上回退到純量路徑),並使用半位元分割查找表以獲得最大吞吐量。

Lloyd-Max 碼本實現的失真率在資訊理論下限(香農失真率極限)的 2.7 倍以內;長度重新歸一化步驟消除了 Lloyd-Max 碼本在內積估計器本身上引入的殘留偏差。

所有 x86_64 版本都以 x86-64-v2(SSE4.2 基線,Nehalem 2008+)為目標,透過 .cargo/config.toml,因此任何 x86-64-v2 CPU 都可以運行整個 crate。AVX-512 和 AVX2 核心是受 #[target_feature] 保護的,並在運行時透過 is_x86_feature_detected! 進行選擇,因此它們可以在支援的硬體上啟動,而與編譯基線無關;不支援兩者的 CPU 將運行純量回退。

每個基準測試都是 benchmarks/suite/ 中一個獨立的腳本。可以單獨運行任何一個:

結果以 JSON 格式儲存到 benchmarks/results/。重新生成圖表:

上面的套件是所有已發布數字的來源——真實嵌入、FAISS 比較器、固定形狀,在兩個官方環境上運行。對於優化過程的內部循環,還有一個 Rust 運行時,它在確定性的合成向量上重現四個變異指標(冷批量添加、預熱追加、單次添加、刪除),因此可以在任何機器上以秒為單位測量一個假設,無需資料集和 FAISS:

這是一個篩選工具,而不是已發布數字的來源。

examples/encode_hash 打印出固定輸入的編碼管道的每個階段的雜湊值;CI 在矩陣中的每個 OS 上運行它,如果它們不同則會失敗,這就是跨平台編碼位元組一致性的檢查方式。

A vector index built on TurboQuant, written in Rust with Python bindings