HomeRessources, Guides & Actualités – Actualités de l’intelligence artificielleMemoTurbovec : Indexation vectorielle ultra-compacte et haute performance avec TurboQuant

Turbovec : Indexation vectorielle ultra-compacte et haute performance avec TurboQuant

Dix millions de vecteurs en float32 occupent trente et un gigaoctets de mémoire vive. Ce volume sature les serveurs standards lors du déploiement de pipelines RAG en production. J’utilise turbovec pour compresser ce même corpus dans quatre gigaoctets de RAM, tout en dépassant la vitesse de recherche de FAISS.

turbovec constitue un moteur d’indexation vectorielle écrit en Rust avec des liaisons Python. Il implémente l’algorithme TurboQuant conçu par Google Research. Cette méthode de quantification ignore la distribution brute des données pour offrir une distorsion proche de la limite théorique de Shannon sans exiger de phase d’entraînement préalable.

Principes fondamentaux et fonctionnement interne

Chaque vecteur représente une direction sur une hypersphère de grande dimension. L’algorithme repose sur une propriété mathématique : l’application d’une rotation aléatoire redistribue chaque coordonnée selon une loi connue et universelle.

Le pipeline de traitement s’articule en six étapes distinctes :

  1. Normalisation : Le système extrait la norme euclidienne du vecteur et stocke ce scalaire float32. Le vecteur devient une direction unitaire sur l’hypersphère.
  2. Rotation aléatoire : Une matrice orthogonale identique multiplie chaque vecteur. Les coordonnées transformées suivent une loi Bêta qui converge vers une distribution gaussienne N(0, 1/d) en haute dimension.
  3. Calibration par coordonnée (TQ+) : En dimension finie, les coordonnées s’écartent de la forme canonique. L’extension TQ+ ajuste deux scalaires par coordonnée pour aligner les quantiles empiriques sur les centroïdes extrêmes du codebook. Un échantillon de 1024 vecteurs suffit pour calculer ce recalage sans réentraînement ultérieur.
  4. Quantification scalaire de Lloyd-Max : La loi statistique étant prédéfinie, les centroïdes et frontières minimisant l’erreur quadratique moyenne sont précalculés de manière statique. Le mode 2-bit utilise 4 intervalles ; le mode 4-bit en emploie 16.
  5. Empaquètement binaire (Bit-packing) : Les indices discrets sont compressés dans des octets. Un vecteur de dimension 1536 passe de 6144 octets en FP32 à 384 octets en 2-bit, réalisant une compression d’un facteur 16.
  6. Score renormalisé par la longueur : La quantification sous-estime le produit scalaire. Le moteur calcule le terme ⟨u, x̂⟩ à l’encodage et stocke le ratio ||v|| / ⟨u, x̂⟩. Le produit scalaire corrigé devient sans biais lors de la recherche, sans surcoût d’évaluation.

La distorsion du codebook de Lloyd-Max reste contenue sous un facteur 2,7 face à la limite débit-distorsion de Shannon.

Lors de la requête, le système projette le vecteur d’interrogation dans l’espace tourné. Les noyaux SIMD calculent les scores sans décompresser les vecteurs de la base.

Installation et compilation

Le paquet officiel s’installe via le gestionnaire standard :

pip install turbovec

Pour compiler les extensions Python depuis le code source avec Maturin :

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

Pour une intégration native en Rust, ajoutez la dépendance dans votre manifeste Cargo :

cargo add turbovec

Le compilateur cible le jeu d’instructions x86-64-v2 par défaut (SSE4.2). Les noyaux optimisés exploitent AVX-512 VNNI, vpermb, AVX2 ou ARM NEON via détection dynamique à l’exécution.

Utilisation en Python

L’API rejette les types non conformes. Vous devez fournir des tableaux NumPy 2D au format float32 strict.

import numpy as np
from turbovec import TurboQuantIndex

# Initialisation de l'index de dimension 1536 en 4-bit
index = TurboQuantIndex(dim=1536, bit_width=4)

# Données d'exemple
vectors = np.random.randn(1000, 1536).astype(np.float32)
queries = np.random.randn(5, 1536).astype(np.float32)

# Ingestion en ligne
index.add(vectors)

# Recherche du top-10
scores, indices = index.search(queries, k=10)

# Persistance complète
index.write("mon_index.tv")
loaded_index = TurboQuantIndex.load("mon_index.tv")

# Sauvegarde incrémentale sur disque
index.sync("mon_index.tv")

Si vous devez gérer des identifiants persistants capables de survivre aux suppressions, utilisez la classe IdMapIndex :

import numpy as np
from turbovec import IdMapIndex

# Création de l'index avec table d'identifiants
id_index = IdMapIndex(dim=1536, bit_width=4)
doc_ids = np.array([1001, 1002, 1003], dtype=np.uint64)
raw_vectors = np.random.randn(3, 1536).astype(np.float32)

# Insertion associée aux identifiants
id_index.add_with_ids(raw_vectors, doc_ids)

# Suppression O(1) sans reconstruction
id_index.remove(1002)

# Recherche renvoyant les identifiants uint64 d'origine
scores, match_ids = id_index.search(raw_vectors[:1], k=2)

# Sauvegarde durable incluant les identifiants
id_index.sync("mon_index.tvim")

Recherche hybride et filtrage SIMD

Le filtrage s’opère au cœur des registres vectoriels par blocs de 32 vecteurs. Si aucun élément d’un bloc ne figure dans la liste d’autorisation, l’évaluation des tables de correspondance est court-circuitée.

import numpy as np
import sqlite3
from turbovec import IdMapIndex

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

# Étape 1 : extraction des identifiants autorisés via SQL
conn = sqlite3.connect("metadata.db")
allowed_tuples = conn.execute("SELECT id FROM documents WHERE tenant_id = 42").fetchall()
allowed_ids = np.array([item[0] for item in allowed_tuples], dtype=np.uint64)

# Étape 2 : recherche dense restreinte
scores, filtered_ids = index.search(query_vector, k=10, allowlist=allowed_ids)

Le nombre de résultats renvoyés équivaut à min(k, n_allowed). Aucun faux résultat ne remplit la sélection.

Intégrations aux écosystèmes RAG

turbovec remplace les moteurs vectoriels en mémoire des bibliothèques de référence sans refonte de code :

  • LangChain : pip install turbovec[langchain] (remplace InMemoryVectorStore)
  • LlamaIndex : pip install turbovec[llama-index] (remplace SimpleVectorStore)
  • Haystack : pip install turbovec[haystack] (remplace InMemoryDocumentStore)
  • Agno : pip install turbovec[agno] (remplace LanceDb)

Implémentation native en Rust

Voici un cycle complet d’indexation, recherche et persistance exécuté en Rust :

use turbovec::{TurboQuantIndex, IdMapIndex};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dim = 1536;
    let bit_width = 4;
    
    // Index standard
    let mut index = TurboQuantIndex::new(dim, bit_width)?;
    let vectors = vec![0.0f32; dim * 100];
    index.add(&vectors);
    
    let queries = vec![0.0f32; dim];
    let (scores, indices) = index.search(&queries, 10);
    index.write("index.tv")?;
    
    // Index avec identifiants explicites
    let mut id_index = IdMapIndex::new(dim, bit_width)?;
    let external_ids = vec![1001, 1002, 1003];
    let id_vectors = vec![0.0f32; dim * 3];
    
    id_index.add_with_ids(&id_vectors, &external_ids)?;
    id_index.remove(1002);
    id_index.write("index.tvim")?;
    
    Ok(())
}

Analyse des performances comparées

Les métriques suivantes proviennent de bancs d’essai sur 100 000 vecteurs et 1 000 requêtes (k=64), comparés à FAISS IndexPQFastScan :

ArchitectureConfigurationGain de vitesse face à FAISSLatence d’insertionLatence de suppression
ARM (Google Axion / c4a)4-bit (SDOT/SMMLA)3,5× plus rapide6,3 à 19,7 µs / vecteur0,44 à 1,22 µs / op
ARM (Google Axion / c4a)2-bit (SDOT/SMMLA)26 % plus rapide4,6 à 16,3 µs (par lot de 100)0,59 à 1,37 µs / op
x86 (Sapphire Rapids)4-bit (AVX-512 VNNI)3,4× plus rapide6,3 à 19,7 µs / vecteur0,44 à 1,22 µs / op
x86 (Sapphire Rapids)2-bit (vpermb LUT)20 % plus rapide4,6 à 16,3 µs (par lot de 100)0,59 à 1,37 µs / op

Sur FAISS, une suppression unitaire avec remove_ids requiert 0,19 à 1,02 seconde car la structure recompacte l’intégralité des codes en mémoire. turbovec exécute cette opération en une microseconde grâce à une suppression par échange (swap-and-pop) sans réallocation globale.

Exécution des tests et validation

Pour reproduire les résultats de laboratoire sur vos machines :

# Téléchargement des jeux de données de référence
python3 benchmarks/download_data.py all

# Lancement des tests de rapidité ARM et x86
for f in benchmarks/suite/speed_*arm*.py; do python3 "$f"; done
for f in benchmarks/suite/speed_*x86*.py; do python3 "$f"; done

# Évaluation du rappel
for f in benchmarks/suite/recall_*.py; do python3 "$f"; done

# Test rapide sans jeu de données externe
cargo run --release --example insert_bench -- --dim 1536 --bits 2

Le script examples/encode_hash garantit l’intégrité binaire du pipeline d’encodage sur l’ensemble des systèmes d’exploitation au sein de l’intégration continue.

Limites et compromis techniques

L’hypothèse asymptotique de distribution Bêta perd en précision sur les petites dimensions comme GloVe (dim=200). Bien que le recalage TQ+ récupère l’avantage à R@1 (0,572 contre 0,564 pour FAISS), ce dernier conserve une légère avance sur des valeurs de k élevées à 2-bit.

L’absence de conversion implicite de types impose une rigueur absolue lors du formatage de vos matrices NumPy. Tout tenseur différent du standard float32 lève une exception immédiate afin d’éviter les allocations parasites.

Leave a Reply

Your email address will not be published. Required fields are marked *