Создание RAG с помощью Milvus, vLLM и Llama 3.1
Калифорнийский университет в Беркли передал vLLM, быструю и простую в использовании библиотеку для вывода и обслуживания LLM, в фонд LF AI & Data Foundation в качестве проекта на стадии инкубации в июле 2024 года. Как проект-участник, мы приветствуем vLLM в семье LF AI & Data! 🎉
Большие языковые модели(LLM) и векторные базы данных обычно используются в паре для создания Retrieval Augmented Generation(RAG), популярной архитектуры приложений для решения проблемы галлюцинаций ИИ. В этом блоге мы расскажем вам, как построить и запустить RAG с помощью Milvus, vLLM и Llama 3.1. Более конкретно, я покажу вам, как встраивать и хранить текстовую информацию в виде векторных вкраплений в Milvus и использовать это векторное хранилище в качестве базы знаний для эффективного извлечения фрагментов текста, относящихся к вопросам пользователя. Наконец, мы используем vLLM для работы с моделью Meta Llama 3.1-8B, чтобы генерировать ответы, дополненные полученным текстом. Давайте погрузимся!
Введение в Milvus, vLLM и Meta's Llama 3.1
Векторная база данных Milvus
Milvus - это распределенная векторная база данных с открытым исходным кодом, предназначенная для хранения, индексации и поиска векторов в рабочих нагрузках генеративного ИИ (GenAI). Способность выполнять гибридный поиск, фильтрацию метаданных, повторное ранжирование и эффективно обрабатывать триллионы векторов делает Milvus лучшим выбором для рабочих нагрузок ИИ и машинного обучения. Milvus можно запускать локально, на кластере или размещать в полностью управляемом облаке Zilliz Cloud.
vLLM
vLLM - это проект с открытым исходным кодом, начатый в лаборатории UC Berkeley SkyLab и направленный на оптимизацию производительности LLM-серверов. В нем используется эффективное управление памятью с помощью PagedAttention, непрерывное пакетирование и оптимизированные ядра CUDA. По сравнению с традиционными методами, vLLM повышает производительность обслуживания до 24 раз, сокращая при этом использование памяти GPU в два раза.
Согласно статье "Эффективное управление памятью при обслуживании больших языковых моделей с помощью PagedAttention", кэш KV использует около 30 % памяти GPU, что приводит к потенциальным проблемам с памятью. Кэш KV хранится в смежной памяти, но изменение его размера может привести к фрагментации памяти, что неэффективно для вычислений.
Изображение 1. Управление памятью KV-кэша в существующих системах ( статья 2023 Paged Attention)
Используя виртуальную память для KV-кэша, vLLM выделяет физическую память GPU только по мере необходимости, устраняя фрагментацию памяти и избегая предварительного распределения. В тестах vLLM превзошел HuggingFace Transformers (HF) и Text Generation Inference (TGI), достигнув пропускной способности в 24 раза выше, чем у HF, и в 3,5 раза выше, чем у TGI, на графических процессорах NVIDIA A10G и A100.
Изображение 2. Пропускная способность сервиса, когда каждый запрос запрашивает три параллельных завершения вывода. vLLM достигает 8,5x-15x более высокой пропускной способности, чем HF, и 3,3x-3,5x более высокой, чем TGI ( блог 2023 vLLM).
Meta's Llama 3.1
Llama 3.1 от Meta была анонсирована 23 июля 2024 года. Модель 405B демонстрирует высочайшую производительность в нескольких публичных бенчмарках и имеет контекстное окно из 128 000 входных токенов с возможностью различного коммерческого использования. Наряду с моделью с 405 миллиардами параметров Meta выпустила обновленные версии Llama3 70B (70 миллиардов параметров) и 8B (8 миллиардов параметров). Весовые коэффициенты моделей доступны для скачивания на сайте Meta.
Ключевым моментом стало то, что точная настройка сгенерированных данных может повысить производительность, но некачественные примеры могут ее снизить. Команда Llama провела большую работу по выявлению и удалению таких плохих примеров, используя саму модель, вспомогательные модели и другие инструменты.
Построение и выполнение RAG-поиска с помощью Milvus
Подготовьте набор данных.
В качестве набора данных для этой демонстрации я использовал официальную документацию Milvus, которую я скачал и сохранил локально.
from langchain.document_loaders import DirectoryLoader
# Load HTML files already saved in a local directory
path = "../../RAG/rtdocs_new/"
global_pattern = '*.html'
loader = DirectoryLoader(path=path, glob=global_pattern)
docs = loader.load()
# Print num documents and a preview.
print(f"loaded {len(docs)} documents")
print(docs[0].page_content)
pprint.pprint(docs[0].metadata)
loaded 22 documents
Why Milvus Docs Tutorials Tools Blog Community Stars0 Try Managed Milvus FREE Search Home v2.4.x About ...
{'source': 'https://milvus.io/docs/quickstart.md'}
Загрузите модель встраивания.
Далее загрузите бесплатную модель встраивания с открытым исходным кодом с сайта HuggingFace.
import torch
from sentence_transformers import SentenceTransformer
# Initialize torch settings for device-agnostic code.
N_GPU = torch.cuda.device_count()
DEVICE = torch.device('cuda:N_GPU' if torch.cuda.is_available() else 'cpu')
# Download the model from huggingface model hub.
model_name = "BAAI/bge-large-en-v1.5"
encoder = SentenceTransformer(model_name, device=DEVICE)
# Get the model parameters and save for later.
EMBEDDING_DIM = encoder.get_sentence_embedding_dimension()
MAX_SEQ_LENGTH_IN_TOKENS = encoder.get_max_seq_length()
# Inspect model parameters.
print(f"model_name: {model_name}")
print(f"EMBEDDING_DIM: {EMBEDDING_DIM}")
print(f"MAX_SEQ_LENGTH: {MAX_SEQ_LENGTH}")
model_name: BAAI/bge-large-en-v1.5
EMBEDDING_DIM: 1024
MAX_SEQ_LENGTH: 512
Разбейте и закодируйте ваши пользовательские данные в виде векторов.
Я использую фиксированную длину 512 символов с 10-процентным перекрытием.
from langchain.text_splitter import RecursiveCharacterTextSplitter
CHUNK_SIZE = 512
chunk_overlap = np.round(CHUNK_SIZE * 0.10, 0)
print(f"chunk_size: {CHUNK_SIZE}, chunk_overlap: {chunk_overlap}")
# Define the splitter.
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=chunk_overlap)
# Chunk the docs.
chunks = child_splitter.split_documents(docs)
print(f"{len(docs)} docs split into {len(chunks)} child documents.")
# Encoder input is doc.page_content as strings.
list_of_strings = [doc.page_content for doc in chunks if hasattr(doc, 'page_content')]
# Embedding inference using HuggingFace encoder.
embeddings = torch.tensor(encoder.encode(list_of_strings))
# Normalize the embeddings.
embeddings = np.array(embeddings / np.linalg.norm(embeddings))
# Milvus expects a list of `numpy.ndarray` of `numpy.float32` numbers.
converted_values = list(map(np.float32, embeddings))
# Create dict_list for Milvus insertion.
dict_list = []
for chunk, vector in zip(chunks, converted_values):
# Assemble embedding vector, original text chunk, metadata.
chunk_dict = {
'chunk': chunk.page_content,
'source': chunk.metadata.get('source', ""),
'vector': vector,
}
dict_list.append(chunk_dict)
chunk_size: 512, chunk_overlap: 51.0
22 docs split into 355 child documents.
Сохраните векторы в Milvus.
Добавьте закодированные векторные вставки в базу данных векторов Milvus.
# Connect a client to the Milvus Lite server.
from pymilvus import MilvusClient
mc = MilvusClient("milvus_demo.db")
# Create a collection with flexible schema and AUTOINDEX.
COLLECTION_NAME = "MilvusDocs"
mc.create_collection(COLLECTION_NAME,
EMBEDDING_DIM,
consistency_level="Eventually",
auto_id=True,
overwrite=True)
# Insert data into the Milvus collection.
print("Start inserting entities")
start_time = time.time()
mc.insert(
COLLECTION_NAME,
data=dict_list,
progress_bar=True)
end_time = time.time()
print(f"Milvus insert time for {len(dict_list)} vectors: ", end="")
print(f"{round(end_time - start_time, 2)} seconds")
Start inserting entities
Milvus insert time for 355 vectors: 0.2 seconds
Выполните поиск векторов.
Задайте вопрос и найдите ближайшие соседние фрагменты из вашей базы знаний в Milvus.
SAMPLE_QUESTION = "What do the parameters for HNSW mean?"
# Embed the question using the same encoder.
query_embeddings = torch.tensor(encoder.encode(SAMPLE_QUESTION))
# Normalize embeddings to unit length.
query_embeddings = F.normalize(query_embeddings, p=2, dim=1)
# Convert the embeddings to list of list of np.float32.
query_embeddings = list(map(np.float32, query_embeddings))
# Define metadata fields you can filter on.
OUTPUT_FIELDS = list(dict_list[0].keys())
OUTPUT_FIELDS.remove('vector')
# Define how many top-k results you want to retrieve.
TOP_K = 2
# Run semantic vector search using your query and the vector database.
results = mc.search(
COLLECTION_NAME,
data=query_embeddings,
output_fields=OUTPUT_FIELDS,
limit=TOP_K,
consistency_level="Eventually")
Полученный результат выглядит так, как показано ниже.
Retrieved result #1
distance = 0.7001987099647522
('Chunk text: layer, finds the node closest to the target in this layer, and'
...
'outgoing')
source: https://milvus.io/docs/index.md
Retrieved result #2
distance = 0.6953287124633789
('Chunk text: this value can improve recall rate at the cost of increased'
...
'to the target')
source: https://milvus.io/docs/index.md
Сборка и выполнение RAG-генерации с помощью vLLM и Llama 3.1-8B
Установите vLLM и модели из HuggingFace
По умолчанию vLLM загружает большие языковые модели из HuggingFace. В общем, если вы хотите использовать новую модель на HuggingFace, вам следует выполнить pip install --upgrade или -U. Кроме того, вам понадобится графический процессор, чтобы проводить анализ моделей Meta's Llama 3.1 с помощью vLLM.
Полный список всех моделей, поддерживаемых vLLM, можно найти на этой странице документации.
# (Recommended) Create a new conda environment.
conda create -n myenv python=3.11 -y
conda activate myenv
# Install vLLM with CUDA 12.1.
pip install -U vllm transformers torch
import vllm, torch
from vllm import LLM, SamplingParams
# Clear the GPU memory cache.
torch.cuda.empty_cache()
# Check the GPU.
!nvidia-smi
Чтобы узнать больше о том, как установить vLLM, смотрите страницу его установки.
Получение токена HuggingFace.
Некоторые модели на HuggingFace, например Meta Llama 3.1, требуют от пользователя принять лицензию, прежде чем он сможет загрузить грузы. Поэтому вы должны создать учетную запись HuggingFace, принять лицензию модели и сгенерировать токен.
При посещении этой страницы Llama3.1 на HuggingFace вы получите сообщение с просьбой согласиться с условиями. Нажмите "Принять лицензию", чтобы принять условия Meta перед загрузкой весов модели. Одобрение обычно занимает менее одного дня.
После получения одобрения вы должны сгенерировать новый токен HuggingFace. Старые токены не будут работать с новыми разрешениями.
Перед установкой vLLM войдите в HuggingFace с новым токеном. Ниже я использовал секреты Colab для хранения токена.
# Login to HuggingFace using your new token.
from huggingface_hub import login
from google.colab import userdata
hf_token = userdata.get('HF_TOKEN')
login(token = hf_token, add_to_git_credential=True)
Запустите генерацию RAG
В демонстрационном примере мы запускаем модель Llama-3.1-8B
, которая требует GPU и значительного объема памяти. Следующий пример был запущен на Google Colab Pro ($10/месяц) с графическим процессором A100. Чтобы узнать больше о том, как запустить vLLM, вы можете ознакомиться с документацией по быстрому запуску.
# 1. Choose a model
MODELTORUN = "meta-llama/Meta-Llama-3.1-8B-Instruct"
# 2. Clear the GPU memory cache, you're going to need it all!
torch.cuda.empty_cache()
# 3. Instantiate a vLLM model instance.
llm = LLM(model=MODELTORUN,
enforce_eager=True,
dtype=torch.bfloat16,
gpu_memory_utilization=0.5,
max_model_len=1000,
seed=415,
max_num_batched_tokens=3000)
Напишите подсказку, используя контексты и источники, полученные из Milvus.
# Separate all the context together by space.
contexts_combined = ' '.join(contexts)
# Lance Martin, LangChain, says put the best contexts at the end.
contexts_combined = ' '.join(reversed(contexts))
# Separate all the unique sources together by comma.
source_combined = ' '.join(reversed(list(dict.fromkeys(sources))))
SYSTEM_PROMPT = f"""First, check if the provided Context is relevant to
the user's question. Second, only if the provided Context is strongly relevant, answer the question using the Context. Otherwise, if the Context is not strongly relevant, answer the question without using the Context.
Be clear, concise, relevant. Answer clearly, in fewer than 2 sentences.
Grounding sources: {source_combined}
Context: {contexts_combined}
User's question: {SAMPLE_QUESTION}
"""
prompts = [SYSTEM_PROMPT]
Теперь сгенерируйте ответ, используя извлеченные фрагменты и исходный вопрос, помещенный в подсказку.
# Sampling parameters
sampling_params = SamplingParams(temperature=0.2, top_p=0.95)
# Invoke the vLLM model.
outputs = llm.generate(prompts, sampling_params)
# Print the outputs.
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
# !r calls repr(), which prints a string inside quotes.
print()
print(f"Question: {SAMPLE_QUESTION!r}")
pprint.pprint(f"Generated text: {generated_text!r}")
Question: 'What do the parameters for HNSW MEAN!?'
Generated text: 'Answer: The parameters for HNSW (Hiera(rchical Navigable Small World Graph) are: '
'* M: The maximum degree of nodes on each layer oof the graph, which can improve '
'recall rate at the cost of increased search time. * efConstruction and ef: '
'These parameters specify a search range when building or searching an index.'
По-моему, ответ выше выглядит идеально!
Если вас заинтересовала эта демонстрация, попробуйте сами и сообщите нам о своих впечатлениях. Вы также можете присоединиться к нашему сообществу Milvus на Discord, чтобы пообщаться со всеми разработчиками GenAI напрямую.
Ссылки
Презентация 2023 vLLM на Ray Summit
Блог vLLM: vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
Полезный блог о запуске сервера vLLM: Развертывание vLLM: пошаговое руководство