Ottimizzare i database vettoriali, migliorare l'intelligenza artificiale generativa guidata da RAG
Questo post è stato pubblicato originariamente sul canale Medium di Intel e viene ripubblicato qui con l'autorizzazione.
Due metodi per ottimizzare il database vettoriale quando si usa RAG
Foto di Ilya Pavlov su Unsplash
Di Cathy Zhang e Dr. Malini Bhandaru Collaboratori: Lin Yang e Changyan Liu
I modelli di IA generativa (Genai), che vengono adottati in modo esponenziale nella nostra vita quotidiana, vengono migliorati dalla retrieval-augmented generation (RAG), una tecnica utilizzata per migliorare l'accuratezza e l'affidabilità delle risposte recuperando fatti da fonti esterne. La RAG aiuta un normale modello linguistico di grandi dimensioni (LLM) a comprendere il contesto e a ridurre le allucinazioni, sfruttando un enorme database di dati non strutturati memorizzati come vettori - una presentazione matematica che aiuta a catturare il contesto e le relazioni tra i dati.
I RAG aiutano a recuperare più informazioni contestuali e quindi a generare risposte migliori, ma i database vettoriali su cui si basano stanno diventando sempre più grandi per fornire contenuti ricchi a cui attingere. Così come gli LLM a trilioni di parametri sono all'orizzonte, i database vettoriali di miliardi di vettori non sono lontani. In qualità di ingegneri ottimizzatori, eravamo curiosi di vedere se potevamo rendere i database vettoriali più performanti, caricare i dati più velocemente e creare indici più rapidi per garantire la velocità di recupero anche quando vengono aggiunti nuovi dati. Questo non solo ridurrebbe i tempi di attesa degli utenti, ma renderebbe anche le soluzioni AI basate su RAG un po' più sostenibili.
In questo articolo scoprirete di più sui database vettoriali e sui relativi framework di benchmarking, sui set di dati per affrontare i diversi aspetti e sugli strumenti utilizzati per l'analisi delle prestazioni: tutto ciò di cui avete bisogno per iniziare a ottimizzare i database vettoriali. Condivideremo anche i nostri risultati di ottimizzazione su due popolari soluzioni di database vettoriali per ispirarvi nel vostro viaggio di ottimizzazione delle prestazioni e dell'impatto sulla sostenibilità.
Capire i database vettoriali
A differenza dei tradizionali database relazionali o non relazionali, in cui i dati sono memorizzati in modo strutturato, un database vettoriale contiene una rappresentazione matematica di singoli elementi di dati, chiamata vettore, costruita utilizzando una funzione di incorporazione o trasformazione. Il vettore rappresenta comunemente caratteristiche o significati semantici e può essere breve o lungo. I database vettoriali effettuano il reperimento dei vettori mediante la ricerca di similarità utilizzando una metrica di distanza (dove più vicina significa che i risultati sono più simili), come la similarità euclidea, il prodotto del punto o il coseno.
Per accelerare il processo di recupero, i dati vettoriali vengono organizzati utilizzando un meccanismo di indicizzazione. Esempi di questi metodi di organizzazione sono, tra gli altri, le strutture piatte, i file invertiti (IVF), i mondi piccoli navigabili gerarchici (HNSW) e l'hashing sensibile alla località (LSH). Ognuno di questi metodi contribuisce all'efficienza e all'efficacia del recupero di vettori simili quando necessario.
Esaminiamo come si utilizza un database di vettori in un sistema GenAI. La Figura 1 illustra sia il caricamento dei dati in un database vettoriale sia il suo utilizzo nel contesto di un'applicazione GenAI. Quando si inserisce il prompt, questo subisce un processo di trasformazione identico a quello utilizzato per generare i vettori nel database. Questo prompt vettoriale trasformato viene poi utilizzato per recuperare vettori simili dal database vettoriale. Questi elementi recuperati fungono essenzialmente da memoria conversazionale, fornendo una cronologia contestuale per i prompt, in modo simile a come operano gli LLM. Questa caratteristica si rivela particolarmente vantaggiosa nell'elaborazione del linguaggio naturale, nella computer vision, nei sistemi di raccomandazione e in altri ambiti che richiedono la comprensione semantica e la corrispondenza dei dati. La richiesta iniziale viene successivamente "fusa" con gli elementi recuperati, fornendo un contesto e aiutando l'LLM a formulare risposte basate sul contesto fornito, anziché basarsi esclusivamente sui dati di addestramento originali.
Figura 1. Architettura dell'applicazione RAG.
I vettori sono memorizzati e indicizzati per un rapido recupero. I database vettoriali sono di due tipi: i database tradizionali che sono stati estesi per memorizzare i vettori e i database vettoriali costruiti appositamente. Alcuni esempi di database tradizionali che supportano i vettori sono Redis, pgvector, Elasticsearch e OpenSearch. Esempi di database vettoriali costruiti ad hoc sono le soluzioni proprietarie Zilliz e Pinecone e i progetti open source Milvus, Weaviate, Qdrant, Faiss e Chroma. È possibile saperne di più sui database vettoriali su GitHub tramite LangChain e OpenAI Cookbook.
Ne analizzeremo uno per categoria, Milvus e Redis.
Migliorare le prestazioni
Prima di immergerci nelle ottimizzazioni, esaminiamo come vengono valutati i database vettoriali, alcuni framework di valutazione e gli strumenti di analisi delle prestazioni disponibili.
Metriche delle prestazioni
Vediamo le principali metriche che possono aiutare a misurare le prestazioni dei database vettoriali.
- Lalatenza di caricamento misura il tempo necessario per caricare i dati nella memoria del database vettoriale e costruire un indice. Un indice è una struttura di dati utilizzata per organizzare e recuperare in modo efficiente i dati vettoriali in base alla loro somiglianza o distanza. I tipi di indici in memoria includono l'indice piatto, IVF_FLAT, IVF_PQ, HNSW, scalable nearest neighbors (ScaNN) e DiskANN.
- Ilrichiamo è la percentuale di corrispondenze vere, o di elementi rilevanti, trovati nei primi K risultati recuperati dall'algoritmo di ricerca. Valori di richiamo più elevati indicano un migliore recupero degli elementi rilevanti.
- Query per secondo (QPS) è la velocità con cui il database vettoriale può elaborare le query in arrivo. Valori più elevati di QPS implicano una migliore capacità di elaborazione delle query e un migliore throughput del sistema.
Quadri di benchmarking
Figura 2. Il quadro di riferimento per il benchmarking dei database vettoriali.
Il benchmarking di un database vettoriale richiede un server di database vettoriale e dei client. Per i nostri test sulle prestazioni abbiamo utilizzato due popolari strumenti open source.
- VectorDBBench: Sviluppato e open source da Zilliz, VectorDBBench aiuta a testare diversi database vettoriali con diversi tipi di indici e fornisce una comoda interfaccia web.
- vector-db-benchmark: Sviluppato e reso disponibile da Qdrant, vector-db-benchmark aiuta a testare diversi database vettoriali tipici per il tipo di indice HNSW. Esegue i test tramite la riga di comando e fornisce un file Docker Compose per semplificare l'avvio dei componenti del server.
Figura 3. Un esempio di comando vector-db-benchmark usato per eseguire il test di benchmark.
Ma il framework di benchmark è solo una parte dell'equazione. Abbiamo bisogno di dati che mettano alla prova diversi aspetti della soluzione di database vettoriale stessa, come la capacità di gestire grandi volumi di dati, diverse dimensioni di vettori e la velocità di recupero.
Insiemi di dati aperti per testare i database vettoriali
I dataset di grandi dimensioni sono ottimi candidati per testare la latenza del carico e l'allocazione delle risorse. Alcuni dataset hanno dati ad alta dimensionalità e sono ottimi per testare la velocità di calcolo della similarità.
I dataset vanno da una dimensione di 25 a una dimensione di 2048. Il dataset LAION, una raccolta di immagini aperta, è stato utilizzato per l'addestramento di modelli deep-neural visivi e linguistici molto grandi, come i modelli generativi a diffusione stabile. Il dataset OpenAI di 5 milioni di vettori, ciascuno con una dimensione di 1536, è stato creato da VectorDBBench eseguendo OpenAI su dati grezzi. Dato che ogni elemento del vettore è di tipo FLOAT, per salvare i soli vettori sono necessari circa 29 GB (5M * 1536 * 4) di memoria, più una quantità simile per contenere gli indici e altri metadati, per un totale di 58 GB di memoria per i test. Quando si utilizza lo strumento vector-db-benchmark, è necessario garantire un'adeguata archiviazione su disco per salvare i risultati.
Per testare la latenza di carico, avevamo bisogno di una grande collezione di vettori, che deep-image-96-angular offre. Per testare le prestazioni della generazione di indici e del calcolo della somiglianza, i vettori ad alta dimensionalità forniscono maggiore stress. A tal fine abbiamo scelto il dataset 500K di vettori a 1536 dimensioni.
Strumenti per le prestazioni
Abbiamo parlato di come stressare il sistema per identificare le metriche di interesse, ma esaminiamo ciò che accade a un livello inferiore: quanto è occupata l'unità di calcolo, il consumo di memoria, le attese sui lock e altro ancora? Questi dati forniscono indizi sul comportamento del database, particolarmente utili per identificare le aree problematiche.
L'utilità top di Linux fornisce informazioni sulle prestazioni del sistema. Tuttavia, lo strumento perf di Linux fornisce una serie di informazioni più approfondite. Per saperne di più, si consiglia di leggere anche gli esempi di perf di Linux e il metodo di analisi della microarchitettura top-down di Intel. Un altro strumento è Intel® vTune™ Profiler, utile per ottimizzare non solo le applicazioni ma anche le prestazioni e la configurazione del sistema per una serie di carichi di lavoro che spaziano dall'HPC al cloud, dall'IoT ai media, allo storage e altro ancora.
Ottimizzazioni del database vettoriale Milvus
Vediamo alcuni esempi di come abbiamo cercato di migliorare le prestazioni del database vettoriale Milvus.
Riduzione dell'overhead del movimento di memoria nella scrittura del buffer del datanode
I proxy del percorso di scrittura di Milvus scrivono i dati in un broker di log tramite MsgStream. I nodi dati consumano quindi i dati, convertendoli e memorizzandoli in segmenti. I segmenti uniscono i dati appena inseriti. La logica di unione alloca un nuovo buffer per contenere/spostare sia i vecchi dati che i nuovi dati da inserire e quindi restituisce il nuovo buffer come vecchi dati per la successiva unione dei dati. In questo modo i vecchi dati diventano sempre più grandi, rendendo più lento il movimento dei dati. I profili di performance hanno mostrato un elevato overhead per questa logica.
Figura 4. La fusione e lo spostamento dei dati nel database vettoriale generano un overhead di prestazioni elevato.
Abbiamo modificato la logica del buffer di fusione per aggiungere direttamente i nuovi dati ai vecchi, evitando di allocare un nuovo buffer e di spostare i vecchi dati di grandi dimensioni. I profili di performance confermano che questa logica non comporta alcun overhead. Le metriche del microcodice metric_CPU operating frequency e metric_CPU utilization indicano un miglioramento coerente con il fatto che il sistema non deve più attendere il lungo movimento della memoria. La latenza di carico è migliorata di oltre il 60%. Il miglioramento è riportato su GitHub.
Figura 5. Con una minore quantità di copie si nota un miglioramento delle prestazioni di oltre il 50% nella latenza di carico.
Creazione di indici invertiti con riduzione dell'overhead di allocazione della memoria
Il motore di ricerca Milvus, Knowhere, impiega l'algoritmo k-means di Elkan per addestrare i cluster di dati per la creazione di indici di file invertiti (IVF). Ogni ciclo di formazione dei dati definisce un numero di iterazioni. Più grande è il conteggio, migliori sono i risultati dell'addestramento. Tuttavia, ciò implica anche che l'algoritmo Elkan sarà chiamato più frequentemente.
L'algoritmo Elkan gestisce l'allocazione e la deallocazione della memoria a ogni esecuzione. In particolare, alloca la memoria per memorizzare la metà della dimensione dei dati della matrice simmetrica, esclusi gli elementi diagonali. In Knowhere, la dimensione della matrice simmetrica utilizzata dall'algoritmo Elkan è impostata a 1024, con una dimensione di memoria di circa 2 MB. Ciò significa che per ogni ciclo di addestramento Elkan alloca e dealloca ripetutamente 2 MB di memoria.
I dati di profilazione indicano una frequente attività di allocazione di memoria di grandi dimensioni. In effetti, hanno innescato l'allocazione di aree di memoria virtuale (VMA), l'allocazione di pagine fisiche, l'impostazione di mappe di pagine e l'aggiornamento delle statistiche dei cgroup di memoria nel kernel. Questo modello di attività di allocazione/deallocazione di memoria di grandi dimensioni può, in alcune situazioni, aggravare la frammentazione della memoria. Si tratta di una tassa significativa.
La struttura IndexFlatElkan è progettata e costruita specificamente per supportare l'algoritmo Elkan. In ogni processo di formazione dei dati viene inizializzata un'istanza di IndexFlatElkan. Per mitigare l'impatto sulle prestazioni derivante dalla frequente allocazione e deallocazione di memoria nell'algoritmo Elkan, abbiamo rifattorizzato la logica del codice, spostando la gestione della memoria al di fuori della funzione dell'algoritmo Elkan nel processo di costruzione di IndexFlatElkan. In questo modo, l'allocazione della memoria avviene una sola volta durante la fase di inizializzazione, mentre tutte le successive chiamate alla funzione dell'algoritmo Elkan vengono eseguite dal processo di formazione dei dati in corso, contribuendo a migliorare la latenza di carico di circa il 3%. Trovate la patch di Knowhere qui.
Accelerazione della ricerca vettoriale di Redis tramite prefetch software
Redis, un popolare archivio tradizionale di dati in-memory a valore-chiave, ha recentemente iniziato a supportare la ricerca vettoriale. Per andare oltre il tipico archivio di valori-chiave, offre moduli di estensibilità; il modulo RediSearch facilita la memorizzazione e la ricerca di vettori direttamente all'interno di Redis.
Per la ricerca di similarità vettoriale, Redis supporta due algoritmi: forza bruta e HNSW. L'algoritmo HNSW è stato creato appositamente per individuare in modo efficiente i vicini approssimativi in spazi ad alta dimensione. Utilizza una coda prioritaria chiamata candidate_set per gestire tutti i candidati vettoriali per il calcolo della distanza.
Ogni candidato vettoriale comprende metadati sostanziali oltre ai dati vettoriali. Di conseguenza, il caricamento di un candidato dalla memoria può causare la perdita di dati dalla cache, con conseguenti ritardi nell'elaborazione. La nostra ottimizzazione introduce il prefetching software per caricare in modo proattivo il candidato successivo durante l'elaborazione di quello attuale. Questo miglioramento ha portato a un miglioramento del throughput del 2-3% per le ricerche di similarità vettoriale in una configurazione Redis a istanza singola. La patch è in fase di upstreaming.
Modifica del comportamento predefinito di GCC per evitare penalizzazioni del codice assembly misto
Per ottenere le massime prestazioni, le sezioni di codice utilizzate di frequente sono spesso scritte a mano in assembly. Tuttavia, quando segmenti diversi di codice vengono scritti da persone diverse o in momenti diversi, le istruzioni utilizzate possono provenire da set di istruzioni assembly incompatibili, come Intel® Advanced Vector Extensions 512 (Intel® AVX-512) e Streaming SIMD Extensions (SSE). Se non viene compilato in modo appropriato, il codice misto comporta una riduzione delle prestazioni. Per saperne di più sul mix di istruzioni Intel AVX e SSE, cliccate qui.
È possibile determinare facilmente se si sta utilizzando codice assembly in modalità mista e se non si è compilato il codice con VZEROUPPER, incorrendo così nella penalizzazione delle prestazioni. Si può osservare con un comando perf come sudo perf stat -e 'assists.sse_avx_mix/event/event=0xc1,umask=0x10/' <workload>. Se il vostro sistema operativo non supporta l'evento, usate cpu/event=0xc1,umask=0x10,name=assists_sse_avx_mix/.
Il compilatore Clang inserisce di default VZEROUPPER, evitando così qualsiasi penalizzazione in modalità mista. Ma il compilatore GCC inserisce VZEROUPPER solo quando vengono specificati i flag di compilazione -O2 o -O3. Abbiamo contattato il team di GCC e spiegato il problema; ora, per impostazione predefinita, gestisce correttamente il codice assembly in modalità mista.
Iniziare a ottimizzare i database vettoriali
I database vettoriali giocano un ruolo fondamentale in GenAI e stanno diventando sempre più grandi per generare risposte di qualità superiore. Per quanto riguarda l'ottimizzazione, le applicazioni di IA non sono diverse da altre applicazioni software, in quanto rivelano i loro segreti quando si utilizzano strumenti standard di analisi delle prestazioni insieme a framework di benchmark e input di stress.
Utilizzando questi strumenti, abbiamo scoperto le trappole delle prestazioni relative all'allocazione di memoria non necessaria, al mancato prefetch delle istruzioni e all'uso di opzioni di compilazione non corrette. Sulla base delle nostre scoperte, abbiamo apportato miglioramenti in upstream a Milvus, Knowhere, Redis e al compilatore GCC per contribuire a rendere l'IA un po' più performante e sostenibile. I database vettoriali sono un'importante classe di applicazioni che meritano di essere ottimizzate. Speriamo che questo articolo vi aiuti a iniziare.
- Capire i database vettoriali
- Migliorare le prestazioni
- Ottimizzazioni del database vettoriale Milvus
- Accelerazione della ricerca vettoriale di Redis tramite prefetch software
- Modifica del comportamento predefinito di GCC per evitare penalizzazioni del codice assembly misto
- Iniziare a ottimizzare i database vettoriali
On This Page
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word