Construire RAG sur l'architecture Arm
Les processeursArm sont largement utilisés dans une large gamme d'applications, y compris les cas d'utilisation traditionnels de l'apprentissage automatique (ML) et de l'intelligence artificielle (AI).
Dans ce tutoriel, vous apprendrez à construire une application RAG (Retrieval-Augmented Generation) sur des infrastructures basées sur l'architecture Arm. Pour le stockage des vecteurs, nous utilisons Zilliz Cloud, la base de données vectorielle Milvus entièrement gérée. Zilliz Cloud est disponible sur les principaux clouds tels que AWS, GCP et Azure. Dans cette démonstration, nous utilisons Zilliz Cloud déployé sur AWS avec des machines Arm. Pour le LLM, nous utilisons le modèle Llama-3.1-8B
sur l'unité centrale du serveur AWS basé sur Arm en utilisant llama.cpp
.
Prérequis
Pour exécuter cet exemple, nous vous recommandons d'utiliser AWS Graviton, qui offre un moyen rentable d'exécuter des charges de travail de ML sur des serveurs basés sur Arm. Ce cahier a été testé sur une instance AWS Graviton3 c7g.2xlarge
avec le système Ubuntu 22.04 LTS.
Vous avez besoin d'au moins quatre cœurs et 8 Go de RAM pour exécuter cet exemple. Configurez un espace de stockage sur disque d'au moins 32 Go. Nous vous recommandons d'utiliser une instance ayant des spécifications identiques ou supérieures.
Après avoir lancé l'instance, connectez-vous à elle et exécutez les commandes suivantes pour préparer l'environnement.
Installez python sur le serveur :
$ sudo apt update
$ sudo apt install python-is-python3 python3-pip python3-venv -y
Créez et activez un environnement virtuel :
$ python -m venv venv
$ source venv/bin/activate
Installer les dépendances python requises :
$ pip install --upgrade pymilvus openai requests langchain-huggingface huggingface_hub tqdm
Chargement des données hors ligne
Créer la collection
Nous utilisons Zilliz Cloud déployé sur AWS avec des machines basées sur Arm pour stocker et récupérer les données vectorielles. Pour démarrer rapidement, il suffit d'ouvrir un compte sur Zilliz Cloud gratuitement.
En plus de Zilliz Cloud, Milvus auto-hébergé est également une option (plus compliquée à mettre en place). Nous pouvons également déployer Milvus Standalone et Kubernetes sur des machines basées sur ARM. Pour plus d'informations sur l'installation de Milvus, veuillez vous référer à la documentation d'installation.
Nous définissons les adresses uri
et token
comme point de terminaison public et clé Api dans Zilliz Cloud.
from pymilvus import MilvusClient
milvus_client = MilvusClient(
uri="<your_zilliz_public_endpoint>", token="<your_zilliz_api_key>"
)
collection_name = "my_rag_collection"
Vérifier si la collection existe déjà et la supprimer si c'est le cas.
if milvus_client.has_collection(collection_name):
milvus_client.drop_collection(collection_name)
Créer une nouvelle collection avec les paramètres spécifiés.
Si nous ne spécifions aucune information de champ, Milvus créera automatiquement un champ par défaut id
pour la clé primaire et un champ vector
pour stocker les données vectorielles. Un champ JSON réservé est utilisé pour stocker les champs non définis par le schéma et leurs valeurs.
milvus_client.create_collection(
collection_name=collection_name,
dimension=384,
metric_type="IP", # Inner product distance
consistency_level="Strong", # Strong consistency level
)
Nous utilisons la distance du produit intérieur comme type métrique par défaut. Pour plus d'informations sur les types de distance, vous pouvez vous référer à la page Métriques de similarité.
Préparer les données
Nous utilisons les pages FAQ de la documentation Milvus 2.4.x comme connaissance privée dans notre RAG, qui est une bonne source de données pour un pipeline RAG simple.
Téléchargez le fichier zip et extrayez les documents dans le dossier milvus_docs
.
$ wget https://github.com/milvus-io/milvus-docs/releases/download/v2.4.6-preview/milvus_docs_2.4.x_en.zip
$ unzip -q milvus_docs_2.4.x_en.zip -d milvus_docs
Nous chargeons tous les fichiers markdown à partir du dossier milvus_docs/en/faq
. Pour chaque document, nous utilisons simplement "# " pour séparer le contenu du fichier, ce qui permet de séparer grossièrement le contenu de chaque partie principale du fichier markdown.
from glob import glob
text_lines = []
for file_path in glob("milvus_docs/en/faq/*.md", recursive=True):
with open(file_path, "r") as file:
file_text = file.read()
text_lines += file_text.split("# ")
Insérer des données
Nous préparons un modèle d'intégration simple mais efficace, all-MiniLM-L6-v2, qui peut convertir le texte en vecteurs d'intégration.
from langchain_huggingface import HuggingFaceEmbeddings
embedding_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
Nous parcourons les lignes de texte, créons des vecteurs d'intégration, puis insérons les données dans Milvus.
Voici un nouveau champ text
, qui est un champ non défini dans le schéma de la collection. Il sera automatiquement ajouté au champ dynamique JSON réservé, qui peut être traité comme un champ normal à un niveau élevé.
from tqdm import tqdm
data = []
text_embeddings = embedding_model.embed_documents(text_lines)
for i, (line, embedding) in enumerate(
tqdm(zip(text_lines, text_embeddings), desc="Creating embeddings")
):
data.append({"id": i, "vector": embedding, "text": line})
milvus_client.insert(collection_name=collection_name, data=data)
Creating embeddings: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 72/72 [00:18<00:00, 3.91it/s]
Lancement du service LLM sur Arm
Dans cette section, nous allons construire et lancer le service llama.cpp
sur l'unité centrale basée sur Arm.
Modèle Llama 3.1 et llama.cpp
Le modèle Llama-3.1-8B de Meta appartient à la famille des modèles Llama 3.1 et est libre d'utilisation pour la recherche et à des fins commerciales. Avant d'utiliser le modèle, visitez le site web Llama et remplissez le formulaire de demande d'accès.
llama.cpp est un projet C/C++ open source qui permet une inférence LLM efficace sur une variété de matériel - à la fois localement et dans le nuage. Vous pouvez facilement héberger un modèle Llama 3.1 en utilisant llama.cpp
.
Téléchargez et compilez llama.cpp
Exécutez les commandes suivantes pour installer make, cmake, gcc, g++ et d'autres outils essentiels requis pour construire llama.cpp à partir des sources :
$ sudo apt install make cmake -y
$ sudo apt install gcc g++ -y
$ sudo apt install build-essential -y
Vous êtes maintenant prêt à construire llama.cpp
.
Clonez le dépôt des sources de llama.cpp :
$ git clone https://github.com/ggerganov/llama.cpp
Par défaut, llama.cpp
ne construit pour le CPU que sous Linux et Windows. Vous n'avez pas besoin de fournir de commutateurs supplémentaires pour le construire pour le processeur Arm sur lequel vous l'exécutez.
Exécutez make
pour le compiler :
$ cd llama.cpp
$ make GGML_NO_LLAMAFILE=1 -j$(nproc)
Vérifiez que llama.cpp
a été compilé correctement en exécutant la commande help :
$ ./llama-cli -h
Si llama.cpp
a été construit correctement, vous verrez l'option help s'afficher. L'extrait de sortie ressemble à ceci :
example usage:
text generation: ./llama-cli -m your_model.gguf -p "I believe the meaning of life is" -n 128
chat (conversation): ./llama-cli -m your_model.gguf -p "You are a helpful assistant" -cnv
Vous pouvez maintenant télécharger le modèle à l'aide de la commande huggingface cli :
$ huggingface-cli download cognitivecomputations/dolphin-2.9.4-llama3.1-8b-gguf dolphin-2.9.4-llama3.1-8b-Q4_0.gguf --local-dir . --local-dir-use-symlinks False
Le format de modèle GGUF, introduit par l'équipe llama.cpp, utilise la compression et la quantification pour réduire la précision des poids à des entiers de 4 bits, réduisant de manière significative les exigences de calcul et de mémoire et rendant les CPU Arm efficaces pour l'inférence LLM.
Re-quantifier les poids du modèle
Pour quantifier à nouveau, exécutez
$ ./llama-quantize --allow-requantize dolphin-2.9.4-llama3.1-8b-Q4_0.gguf dolphin-2.9.4-llama3.1-8b-Q4_0_8_8.gguf Q4_0_8_8
Ceci produira un nouveau fichier, dolphin-2.9.4-llama3.1-8b-Q4_0_8_8.gguf
, qui contient des poids reconfigurés qui permettent à llama-cli
d'utiliser SVE 256 et le support MATMUL_INT8.
Cette requantification est optimale spécifiquement pour Graviton3. Pour Graviton2, la requantification optimale doit être effectuée au format Q4_0_4_4
, et pour Graviton4, le format Q4_0_4_8
est le plus approprié pour la requantification.
Démarrer le service LLM
Vous pouvez utiliser le programme serveur llama.cpp et envoyer des requêtes via une API compatible avec l'OpenAI. Cela vous permet de développer des applications qui interagissent avec le LLM plusieurs fois sans avoir à le démarrer et à l'arrêter de manière répétée. En outre, vous pouvez accéder au serveur à partir d'une autre machine où le LLM est hébergé sur le réseau.
Démarrez le serveur à partir de la ligne de commande, et il écoute sur le port 8080 :
$ ./llama-server -m dolphin-2.9.4-llama3.1-8b-Q4_0_8_8.gguf -n 2048 -t 64 -c 65536 --port 8080
'main: server is listening on 127.0.0.1:8080 - starting the main loop
Vous pouvez également ajuster les paramètres du LLM lancé pour l'adapter au matériel de votre serveur et obtenir des performances optimales. Pour plus d'informations sur les paramètres, voir la commande llama-server --help
.
Si vous avez du mal à réaliser cette étape, vous pouvez vous référer aux documents officiels pour plus d'informations.
Vous avez démarré le service LLM sur votre processeur Arm. Nous allons maintenant interagir directement avec le service en utilisant le SDK OpenAI.
RAG en ligne
Client LLM et modèle d'intégration
Nous initialisons le client LLM et préparons le modèle d'intégration.
Pour le LLM, nous utilisons le SDK OpenAI pour demander le service Llama lancé précédemment. Nous n'avons pas besoin d'utiliser de clé API car il s'agit en fait de notre service local llama.cpp.
from openai import OpenAI
llm_client = OpenAI(base_url="http://localhost:8080/v1", api_key="no-key")
Générer un embedding de test et imprimer sa dimension et ses premiers éléments.
test_embedding = embedding_model.embed_query("This is a test")
embedding_dim = len(test_embedding)
print(embedding_dim)
print(test_embedding[:10])
384
[0.03061249852180481, 0.013831384479999542, -0.02084377221763134, 0.016327863559126854, -0.010231520049273968, -0.0479842908680439, -0.017313342541456223, 0.03728749603033066, 0.04588735103607178, 0.034405000507831573]
Récupérer des données pour une requête
Spécifions une question fréquente sur Milvus.
question = "How is data stored in milvus?"
Recherchez la question dans la collection et récupérez les 3 meilleures réponses sémantiques.
search_res = milvus_client.search(
collection_name=collection_name,
data=[
embedding_model.embed_query(question)
], # Use the `emb_text` function to convert the question to an embedding vector
limit=3, # Return top 3 results
search_params={"metric_type": "IP", "params": {}}, # Inner product distance
output_fields=["text"], # Return the text field
)
Jetons un coup d'œil aux résultats de la recherche de la requête
import json
retrieved_lines_with_distances = [
(res["entity"]["text"], res["distance"]) for res in search_res[0]
]
print(json.dumps(retrieved_lines_with_distances, indent=4))
[
[
" Where does Milvus store data?\n\nMilvus deals with two types of data, inserted data and metadata. \n\nInserted data, including vector data, scalar data, and collection-specific schema, are stored in persistent storage as incremental log. Milvus supports multiple object storage backends, including [MinIO](https://min.io/), [AWS S3](https://aws.amazon.com/s3/?nc1=h_ls), [Google Cloud Storage](https://cloud.google.com/storage?hl=en#object-storage-for-companies-of-all-sizes) (GCS), [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs), [Alibaba Cloud OSS](https://www.alibabacloud.com/product/object-storage-service), and [Tencent Cloud Object Storage](https://www.tencentcloud.com/products/cos) (COS).\n\nMetadata are generated within Milvus. Each Milvus module has its own metadata that are stored in etcd.\n\n###",
0.6488019824028015
],
[
"How does Milvus flush data?\n\nMilvus returns success when inserted data are loaded to the message queue. However, the data are not yet flushed to the disk. Then Milvus' data node writes the data in the message queue to persistent storage as incremental logs. If `flush()` is called, the data node is forced to write all data in the message queue to persistent storage immediately.\n\n###",
0.5974207520484924
],
[
"What is the maximum dataset size Milvus can handle?\n\n \nTheoretically, the maximum dataset size Milvus can handle is determined by the hardware it is run on, specifically system memory and storage:\n\n- Milvus loads all specified collections and partitions into memory before running queries. Therefore, memory size determines the maximum amount of data Milvus can query.\n- When new entities and and collection-related schema (currently only MinIO is supported for data persistence) are added to Milvus, system storage determines the maximum allowable size of inserted data.\n\n###",
0.5833579301834106
]
]
Utiliser LLM pour obtenir une réponse RAG
Convertissez les documents récupérés dans un format de chaîne.
context = "\n".join(
[line_with_distance[0] for line_with_distance in retrieved_lines_with_distances]
)
Define system and user prompts for the Language Model. This prompt is assembled with the retrieved documents from Milvus.
SYSTEM_PROMPT = """
Human: You are an AI assistant. You are able to find answers to the questions from the contextual passage snippets provided.
"""
USER_PROMPT = f"""
Use the following pieces of information enclosed in <context> tags to provide an answer to the question enclosed in <question> tags.
<context>
{context}
</context>
<question>
{question}
</question>
"""
Utilisez LLM pour générer une réponse basée sur les invites. Nous avons défini le paramètre model
à not-used
car il s'agit d'un paramètre redondant pour le service llama.cpp.
response = llm_client.chat.completions.create(
model="not-used",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_PROMPT},
],
)
print(response.choices[0].message.content)
Milvus stores data in two types: inserted data and metadata. Inserted data, including vector data, scalar data, and collection-specific schema, are stored in persistent storage as incremental log. Milvus supports multiple object storage backends such as MinIO, AWS S3, Google Cloud Storage (GCS), Azure Blob Storage, Alibaba Cloud OSS, and Tencent Cloud Object Storage (COS). Metadata are generated within Milvus and each Milvus module has its own metadata that are stored in etcd.
Félicitations ! Vous avez construit une application RAG sur les infrastructures basées sur Arm.