最近,DeepSeek v3(一个 MoE 模型,拥有 671B 参数,其中 37B 参数被激活)模型全球爆火。
作为一款能与 Claude 3.5 Sonnet,GPT-4o 等模型匹敌的开源模型 DeepSeek v3 不仅将其算法开源,还放出一份扎实的技术报告,详尽描述了 DeepSeek 是如何进行大模型架构、算法工程协同设计,部署,训练,数据处理等方面的思考,堪称是一份 DeepSeek 给开源社区送上的年末大礼。
本篇文章,我们会对 DeepSeek v3 的亮点进行梳理,并对其 RAG 搭建流程与效果,做一个简单的示例。
DeepSeek v3 的亮点
亮点一:超低的训练成本,将带来算力的极大富余
相比于海外大厂动辄上万甚至上十万的 H100 集群(例如 Meta 使用了 16K 的 H100 训练 Llama3.1 405B),DeepSeek 仅仅使用了 2048 张丐版显卡 H800 就在 14.8T 数据上训练了一个处于第一梯队的开源模型。以下是 DeepSeek v3 的训练成本数据。
不难看出,基于以上数据,传统对大模型对算力的供需预测推演直接被推翻,过去 Scaling law 曲线所估算出的 GPU 需求数量会出现极大冗余。
那么问题来了,DeepSeek v3 是如何做到的?
亮点二:颠覆 GPT 架构,极致的工程设计
在去年,大模型领域普遍认为模型的设计已经收敛到 Decoder-only 的 GPT 架构,但 DeepSeek 依然没有放弃对模型架构的进一步探索。
这一次 V3 的设计延用了 V2 提出的 MLA(Multi-head Latent Attention),这是一种通过低秩压缩键值对来减少缓存需求的创新架构,以提高 Transformer 模型的推理效率。
另外,此次的 MoE 模型规格也比之前大了许多(V3 671B, V2 236B),也体现出了对这个架构拥有更多的信心和经验。DeepSeek V3 将除前三层外的所有 FFN 层替换为 MoE 层。每个 MoE 层包含 1 个共享专家和 256 个路由专家。在路由专家中,每个 token 将激活 8 个专家,并确保每个 token 最多会被发送到 4 个节点。
同时,论文还对如何在系统中设计将这种架构进行推理的性能优化也进行了详尽的描述。
DeepSeek V3 使用了多 token 预测(MTP),即每个 token 除了精确预测下一个 token 外,还会预测一个额外的 token,通过投机采样的方式提高推理效率。
关于如何使用 FP8 进行模型训练这个各个大模型工程团队头痛的问题,DeepSeek V3 也对自己的实践有细致的描述,对这部分感兴趣的朋友强烈推荐阅读论文原文。
亮点三:通过蒸馏推理模型进行后训练
自从 OpenAI 发布了 o1 模型之后,业界开始逐渐兴起了探索这种内置思维琏(CoT)的模型,它不断对中间结果探索分析的过程仿佛人的“慢思考”。DeepSeek 同样也开发了类似的 R1 模型,在 DeepSeek V3 中,DeepSeek 创新性地通过在后训练阶段使用 R1 得到的高质量答案来提高了自身的性能。这一点也非常有趣。
众所周知,类似 o1 的开源模型大部分都是从基础模型利用 CoT 结合强化学习的技巧训练出来提高了推理效果,而现在又通过蒸馏推理模型获得了下一代更好的基础模型,这一种模型和数据质量互相交织的发展模式贯穿着机器学习发展的历史,而还将继续被见证。
而以发掘非结构化数据价值的厂商 Zilliz 也相信对于数据和知识的高效管理,将会一直在智能化浪潮发展中扮演着重要的角色。
看到 10K$的后训练成本,相信许多致力于微调专属大模型的厂商都跃跃欲试,在这里我们也来看一下 DeepSeek V3 的后训练过程,整个流程也比传统的 SFT 要复杂一些。整个过程分成了 SFT 阶段(监督学习)以及 RL 阶段(强化学习),在 SFT 阶段,他们将数据分成了两种类型,推理数据以及非推理数据
推理数据:
包括数学,编程这些问题,DeepSeek 训练了针对性的专家模型,并使用专家模型为每一个问题生成了两种格式的学习数据。
<problem, original response>
<system prompt, problem, R1 response>
非推理数据:
对于非推理任务(如创意写作和简单问答),作者利用 DeepSeek-V2.5 模型生成初步响应,并聘请人工标注员对其准确性进行验证
训练的流程:
SFT 阶段,使用基于专家模型生成的 SFT 样本,进行初步的监督微调。通过这些训练数据,模型学习如何根据问题和回答生成精确的推理响应。
RL 阶段,使用高温采样来生成响应,这些响应融合了来自原始数据和 R1 生成数据的模式。在 RL 阶段,会使用 LeetCode 编译器来检查编程的答案,以及一些规则来去检查数学问题的答案,对于开放性问题, 会用一个奖励模型来去判断。该过程帮助模型在没有显式系统提示的情况下进行推理,经过数百次 RL 步骤,模型学会如何平衡准确与简洁性的答案。
完成 RL 训练后,作者实施拒绝采样策略(过滤掉模型认为低质量的数据),以从生成的样本中挑选出高质量的 SFT 数据。这些数据用于最终模型的微调。
不难发现,做好一个高质量的后训练,下的功夫远远不止 10k$的训练算力。
DeepSeek V3 虽然拥有可以与闭源模型匹敌的性能,但是部署它依然不是一个简单的事,即使作者已经为了推理优化做了许多工作,但搭建一个 DeepSeek V3 的服务(考虑到它 671B 的参数量),成本依然不低。
使用 Milvus 和 DeepSeek 搭建 RAG
接下来,我们将展示如何使用 Milvus 和 DeepSeek 构建检索增强生成(RAG)pipeline。
准备
依赖和环境
pip install --upgrade pymilvus[model] openai requests tqdm
复制代码
如果您使用的是 Google Colab,要启用刚刚安装的依赖项,您可能需要重启运行环境(单击屏幕顶部的“Runtime”菜单,然后从下拉框中选择“Restart session”)。
DeepSeek 启动了 OpenAI 风格的 API。您可以登录官网并将api密钥 DEEPSEEK_API_KEY 准备为环境变量。
import os
os.environ["DEEPSEEK_API_KEY"] = "***********"
复制代码
准备数据
我们使用Milvus文档2.4. x中的 FAQ 页面作为 RAG 中的私有知识,这是搭建一个入门 RAG pipeline 的优质数据源。
首先,下载 zip 文件并将文档解压缩到文件夹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
复制代码
我们从文件夹milvus_docs/en/faq
中加载所有 markdown 文件,对于每个文档,我们只需简单地使用“#”来分隔文件中的内容,就可以大致分隔 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("# ")
复制代码
准备 LLM 和 embedding 模型
DeepSeek 采用了类 OpenAI 风格的 API,您可以使用相同的 API 并对相应的 LLM 进行微调。
from openai import OpenAI
deepseek_client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com",
)
复制代码
选择一个 embedding 模型,使用milvus_model
来做文本向量化。我们以DefaultEmbeddingFunction
模型为例,它是一个预训练的轻量级 embedding 模型。
from pymilvus import model as milvus_model
embedding_model = milvus_model.DefaultEmbeddingFunction()
复制代码
生成测试向量,并输出向量维度以及测试向量的前几个元素。
test_embedding = embedding_model.encode_queries(["This is a test"])[0]
embedding_dim = len(test_embedding)
print(embedding_dim)
print(test_embedding[:10])
复制代码
768
[-0.04836066 0.07163023 -0.01130064 -0.03789345 -0.03320649 -0.01318448
-0.03041712 -0.02269499 -0.02317863 -0.00426028]
复制代码
将数据加载到 Milvus
创建集合
from pymilvus import MilvusClient
milvus_client = MilvusClient(uri="./milvus_demo.db")
collection_name = "my_rag_collection"
复制代码
对于MilvusClient
需要说明:
将uri
设置为本地文件,例如./milvus. db
,是最方便的方法,因为它会自动使用 Milvus Lite 将所有数据存储在此文件中。
如果你有大规模数据,你可以在 docker 或 kubernetes 上设置一个更高性能的 Milvus 服务器。在此设置中,请使用服务器 uri,例如http://localhost:19530
,作为你的uri
。
如果要使用 Milvus 的全托管云服务 Zilliz Cloud,请调整uri
和token
,分别对应 Zilliz Cloud 中的公共端点和 Api 密钥。
检查集合是否已经存在,如果存在则将其删除。
if milvus_client.has_collection(collection_name):
milvus_client.drop_collection(collection_name)
复制代码
使用指定的参数创建一个新集合。
如果我们不指定任何字段信息,Milvus 将自动为主键创建一个默认的 id 字段,并创建一个向量字段来存储向量数据。保留的 JSON 字段用于存储未在 schema 里定义的标量数据。
milvus_client.create_collection(
collection_name=collection_name,
dimension=embedding_dim,
metric_type="IP", # Inner product distance
consistency_level="Strong", # Strong consistency level
)
复制代码
插入数据
逐条取出文本数据,创建嵌入,然后将数据插入 Milvus。
这里有一个新的字段“text”,它是集合 schema 中的非定义字段,会自动添加到保留的 JSON 动态字段中。
from tqdm import tqdm
data = []
doc_embeddings = embedding_model.encode_documents(text_lines)
for i, line in enumerate(tqdm(text_lines, desc="Creating embeddings")):
data.append({"id": i, "vector": doc_embeddings[i], "text": line})
milvus_client.insert(collection_name=collection_name, data=data)
复制代码
Creating embeddings: 0%| | 0/72 [00:00<?, ?it/s]huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...
To disable this warning, you can either:
- Avoid using `tokenizers` before the fork if possible
- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)
Creating embeddings: 100%|██████████| 72/72 [00:00<00:00, 246522.36it/s]
{'insert_count': 72, 'ids': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71], 'cost': 0}
复制代码
构建 RAG
检索查询数据
让我们指定一个关于 Milvus 的常见问题。
question = "How is data stored in milvus?"
复制代码
在集合中搜索问题并检索语义 top-3 匹配项。
search_res = milvus_client.search(
collection_name=collection_name,
data=embedding_model.encode_queries(
[question]
), # 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
)
复制代码
我们来看一下 query 的搜索结果
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.6572665572166443
],
[
"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.6312146186828613
],
[
"How does Milvus handle vector data types and precision?\n\nMilvus supports Binary, Float32, Float16, and BFloat16 vector types.\n\n- Binary vectors: Store binary data as sequences of 0s and 1s, used in image processing and information retrieval.\n- Float32 vectors: Default storage with a precision of about 7 decimal digits. Even Float64 values are stored with Float32 precision, leading to potential precision loss upon retrieval.\n- Float16 and BFloat16 vectors: Offer reduced precision and memory usage. Float16 is suitable for applications with limited bandwidth and storage, while BFloat16 balances range and efficiency, commonly used in deep learning to reduce computational requirements without significantly impacting accuracy.\n\n###",
0.6115777492523193
]
]
复制代码
使用 LLM 获取 RAG 响应
将检索到的文档转换为字符串格式。
context = "\n".join(
[line_with_distance[0] for line_with_distance in retrieved_lines_with_distances]
)
复制代码
为 LLM 定义系统和用户提示。这个提示是由从 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>
"""
复制代码
使用 DeepSeek 提供的deepseek-chat
模型根据提示生成响应。
response = deepseek_client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_PROMPT},
],
)
print(response.choices[0].message.content)
复制代码
In Milvus, data is stored in two main categories: inserted data and metadata.
1. **Inserted Data**: This includes vector data, scalar data, and collection-specific schema. The inserted data is stored in persistent storage as incremental logs. Milvus supports various object storage backends for this purpose, such as MinIO, AWS S3, Google Cloud Storage (GCS), Azure Blob Storage, Alibaba Cloud OSS, and Tencent Cloud Object Storage (COS).
2. **Metadata**: Metadata is generated within Milvus and is specific to each Milvus module. This metadata is stored in etcd, a distributed key-value store.
Additionally, when data is inserted, it is first loaded into a message queue, and Milvus returns success at this stage. The data is then written to persistent storage as incremental logs by the data node. If the `flush()` function is called, the data node is forced to write all data in the message queue to persistent storage immediately.
复制代码
太好了!现在我们已经成功使用 Milvus 和 DeepSeek 构建了一个 RAG pipeline。
作者介绍
王翔宇
Zilliz 算法工程师
评论