Aller au contenu
Mohamed Tahiri
Miami --:--
← Tous les articles

Combien de RAM pour dix millions de vecteurs ?

Un index vectoriel se dimensionne avec une multiplication que peu de gens font avant la première facture. Ce que pèsent les vecteurs, le graphe HNSW, et ce que la quantification change vraiment. Avec un calculateur.

La première question qu’on se pose sur une base vectorielle porte sur la qualité des résultats. La deuxième arrive avec la facture : pourquoi ce serveur a-t-il besoin de 64 Go de mémoire ?

La réponse tient en une multiplication. Dix millions de passages, des embeddings à 1 024 dimensions, quatre octets par nombre : 41 Go, rien que pour les vecteurs. Avant l’index, avant les métadonnées, avant la marge du moteur. C’est le calcul à faire au moment de choisir un modèle d’embedding, pas six mois après.

Cet article décompose ce qui occupe la mémoire d’un index vectoriel, puis ce qu’on peut réellement gagner, et à quel prix. Les ordres de grandeur viennent de ce que j’ai appris en construisant Buddy, dont la recherche repose sur Qdrant, mais ils valent pour la plupart des moteurs.

Ce qui occupe la mémoire

D’abord les vecteurs eux-mêmes : nombre de vecteurs × dimensions × octets par dimension. Par défaut, les API d’embedding renvoient des nombres à virgule flottante sur 32 bits, donc quatre octets chacun. Un vecteur de 1 024 dimensions pèse 4 Ko.

Ensuite l’index. La plupart des moteurs utilisent HNSW, un graphe où chaque vecteur garde la liste de ses voisins proches. La recherche part d’un point d’entrée et saute de voisin en voisin vers la question. Avec le réglage courant m = 16, chaque point garde de l’ordre d’une trentaine de voisins sur la couche principale, chacun stocké comme un identifiant de quatre octets. Comptez environ 140 octets par vecteur, quelle que soit sa taille.

Restent les métadonnées et la marge propre à chaque moteur, que le calculateur ignore. La documentation de Qdrant propose une règle simple pour les inclure : la taille brute des vecteurs, multipliée par 1,5.

Moins de dimensions

La dimension d’un embedding est souvent un choix, pas une fatalité. Certains modèles récents sont entraînés pour que les premières dimensions portent l’essentiel de l’information, une technique appelée Matryoshka, en référence aux poupées russes. On peut alors tronquer un vecteur à 512 ou 256 dimensions en perdant peu. OpenAI et Voyage, par exemple, proposent un paramètre pour choisir la dimension de sortie.

Diviser la dimension par deux divise la mémoire des vecteurs par deux, et accélère chaque comparaison d’autant. Le « peu » de la perte dépend de vos données, donc il se mesure sur un jeu d’évaluation fait de vraies questions, pas sur le benchmark du fournisseur.

Moins d’octets par dimension

La quantification garde toutes les dimensions mais les stocke avec moins de précision. En float16, on divise par deux, sans perte mesurable dans la plupart des cas. En int8, chaque nombre est ramené à 256 niveaux à partir de la plage de valeurs observée : quatre fois moins de mémoire, pour une perte en général faible. C’est le réglage que j’active presque par défaut.

La quantification binaire va beaucoup plus loin : un seul bit par dimension, son signe. Trente-deux fois moins de mémoire, et des comparaisons qui se réduisent à quelques opérations sur des bits, extrêmement rapides. Mais l’information perdue est réelle. Elle passe mieux sur les modèles à grandes dimensions, et peut faire mal sur un petit modèle à 384 dimensions.

D’où le reclassement. On cherche avec les codes binaires en demandant trois ou quatre fois plus de candidats que nécessaire, puis on recalcule le score exact de ces candidats avec les vecteurs d’origine. La recherche grossière trouve le bon voisinage, le calcul exact remet l’ordre.

Où vivent les originaux

Quantifier ne veut pas dire jeter les vecteurs d’origine. On les déplace sur disque. En mémoire restent les codes compressés et le graphe, ce qui sert à chaque recherche. Sur le SSD restent les vecteurs complets, lus seulement pour reclasser une poignée de candidats. Le disque coûte une fraction du prix de la RAM, et garder les originaux rend la quantification réversible : si le rappel déçoit, on change de réglage sans recalculer les embeddings.

Avec Qdrant, cela tient en quelques lignes de configuration :

collection.py
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="passages",
    vectors_config=models.VectorParams(
        size=1024,
        distance=models.Distance.COSINE,
        on_disk=True,  # float32 originals live on disk, read only to rescore
    ),
    quantization_config=models.BinaryQuantization(
        binary=models.BinaryQuantizationConfig(always_ram=True),
    ),
)

hits = client.query_points(
    collection_name="passages",
    query=query_vector,
    limit=10,
    search_params=models.SearchParams(
        # fetch 3x more candidates with the binary codes, then rerank them exactly
        quantization=models.QuantizationSearchParams(rescore=True, oversampling=3.0),
    ),
)

Mesurer le rappel, pas seulement la mémoire

Chaque gain de mémoire se paie en rappel, la part des vrais plus proches voisins que l’index retrouve. La mesure est simple : pour quelques centaines de questions, on compare les dix résultats de l’index réglé aux dix résultats d’une recherche exacte, sans index ni quantification. Si le rappel à 10 passe de 0,98 à 0,95, il faut encore vérifier que la bonne réponse reste dans les cinq premiers résultats de bout en bout : c’est la seule métrique que l’utilisateur ressent.

10 M × 1 024 dimensions en float32
41 Go
les mêmes en int8
10 Go
les mêmes en binaire
1,3 Go

Ma méthode

  • Faire la multiplication avant de choisir le modèle d’embedding.
  • Tester une dimension réduite sur ses propres questions avant de payer pour la dimension pleine.
  • Passer en int8 presque par défaut, en binaire seulement avec reclassement, et après mesure.
  • Garder les originaux sur disque : ils coûtent peu et laissent toutes les options ouvertes.
  • Se souvenir que le graphe a son propre poids, qui ne diminue pas avec la quantification.
Contact

Construisons quelque chose.

Ou laissez un message