🚀 免費嘗試 Zilliz Cloud,完全托管的 Milvus,體驗速度提升 10 倍!立即嘗試

milvus-logo
LFAI
主頁
  • 整合

使用 Milvus、vLLM 和 Llama 3.1 建立 RAG

加州大學柏克萊分校於 2024 年 7 月捐贈vLLMLF AI & Data Foundation作為孵化階段專案。身為其他成員專案,我們歡迎 vLLM 加入 LF AI & Data 大家庭!🎉

大型語言模型(LLM) 與向量資料庫通常會搭配來建構 Retrieval Augmented Generation(RAG),這是一種常用的 AI 應用架構,可以解決AI 幻覺的問題。這篇部落格將會告訴您如何使用 Milvus、vLLM 和 Llama 3.1 來建立並執行 RAG。更具體來說,我會告訴您如何在 Milvus 中將文字資訊嵌入並儲存為向量嵌入,並使用此向量儲存作為知識庫,以有效率地擷取與使用者問題相關的文字塊。最後,我們會利用 vLLM 為 Meta 的 Llama 3.1-8B 模型提供服務,以產生由擷取的文字所增強的答案。讓我們深入瞭解!

Milvus、vLLM 和 Meta's Llama 3.1 簡介

Milvus 向量資料庫

Milvus是一個開放原始碼、專門為 生成式人工智能(GenAI) 工作負載而設計的分散式向量資料庫,用於儲存、索引和搜尋向量。它能夠執行混合搜尋、 元資料過濾、重新排列,並有效率地處理數以萬億計的向量,讓 Milvus 成為 AI 與機器學習工作負載的首選。Milvus可在本機、集群上執行,或託管於全面管理的Zilliz Cloud

vLLM

vLLM是 UC Berkeley SkyLab 開發的開放原始碼專案,專注於優化 LLM 服務效能。它使用 PagedAttention、連續批次和最佳化的 CUDA 核心進行有效的記憶體管理。與傳統方法相比,vLLM 可將服務效能提升 24 倍,同時將 GPU 記憶體使用量減少一半。

根據「Efficient Memory Management for Large Language Model Serving with PagedAttention」這篇論文,KV 快取記憶體使用了約 30% 的 GPU 記憶體,導致潛在的記憶體問題。KV 快取儲存在連續的記憶體中,但改變大小會造成記憶體碎片,對於計算而言效率不高。

圖 1.現有系統中的 KV 快取記憶體管理 (2023 分頁注意事項論文)

透過為 KV 快取記憶體使用虛擬記憶體,vLLM 只會在需要時分配實體 GPU 記憶體,消除記憶體碎片並避免預先分配。在測試中,vLLM 的表現優於HuggingFace Transformers(HF) 和Text Generation Inference(TGI),在 NVIDIA A10G 和 A100 GPU 上,vLLM 的吞吐量比 HF 高出 24 倍,比 TGI 高出 3.5 倍。

圖 2.vLLM 的吞吐量比 HF 高出 8.5 倍至 15 倍,比 TGI 高出 3.3 倍至 3.5 倍 (2023vLLM 博客)。

Meta's Llama 3.1

Meta's Llama 3.1於 2024 年 7 月 23 日發表。405B 模型在多個公開基準上提供最先進的效能,並擁有 128,000 個輸入代幣的上下文視窗,允許各種商業用途。除了 4050 億參數模型之外,Meta 還發布了 Llama3 70B (700 億參數) 和 8B (80 億參數) 的更新版本。模型權重可在 Meta 網站上下載。

一個重要的啟示是,微調產生的資料可以提升效能,但品質不佳的範例則會降低效能。Llama 團隊使用模型本身、輔助模型和其他工具,廣泛地識別和移除這些不良範例。

使用 Milvus 建立並執行 RAG-Retrieval

準備您的資料集。

我使用官方的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

使用 vLLM 和 Llama 3.1-8B 建立並執行 RAG-Generation

從 HuggingFace 安裝 vLLM 和模型

vLLM 預設會從 HuggingFace 下載大型語言模型。一般而言,無論何時您想要在 HuggingFace 上使用全新的模型,都應該執行 pip install --upgrade 或 -U。此外,您需要 GPU 才能使用 vLLM 執行 Meta's Llama 3.1 模型的推論。

如需所有 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 帳戶,接受模型的授權,並產生一個代用幣。

造訪 HuggingFace 上這個Llama3.1 頁面時,您會收到要求您同意條款的訊息。在下載模型權重之前,按一下「接受授權」以接受 Meta 條款。批准通常需要不到一天的時間。

收到批准後,您必須產生一個新的 HuggingFace 令牌。您的舊權限將無法使用新的權限。

在安裝 vLLM 之前,請使用新的 token 登入 HuggingFace。以下,我使用 Colab secrets 來儲存代用幣。

# 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-Generation

在示範中,我們執行Llama-3.1-8B 模型,這需要 GPU 和相當大的記憶體才能啟動。以下範例是在 Google Colab Pro ($10/month) 搭配 A100 GPU 上執行。若要進一步瞭解如何執行 vLLM,您可以查看Quickstart 文件

# 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.'

我覺得上面的答案看起來很完美!

如果您對這個示範有興趣,歡迎親自試用,並讓我們知道您的想法。我們也歡迎您加入Discord 上的 Milvus 社群,直接與所有 GenAI 開發人員交談。

參考資料

免費嘗試托管的 Milvus

Zilliz Cloud 無縫接入,由 Milvus 提供動力,速度提升 10 倍。

開始使用
反饋

這個頁面有幫助嗎?