在华为的昇腾服务器上部署了DeepSeek R1的模型进行验证测试,记录一下相关的过程。服务器是配置了8块910B3的显卡,每块显卡有64GB显存,根据DeepSeek R1各个模型的参数计算,如果部署R1的Qwen 14B版本,需要1张显卡,如果是32B版本,需要2张,Llama 70B的模型需要4张显卡。如果是R1全参数版本,则需要32张显卡,也就是4台满配的昇腾服务器。这里先选择32B的模型进行部署测试,等之后申请更多的算力资源之后再测试部署全参数版本。另外除了部署32B之外,为了能更好地使用DeepSeek,还部署了一个语义嵌入向量模型Bge-zh和向量数据库Chromadb,用于对文本进行编码,方便构建知识库,以及部署了Open Webui服务,使得用户可以访问Webui的方式来使用DeepSeek。
DeepSeek的部署
首先简单介绍一下DeepSeek模型的一些技术特点,包括了
- 基于Transformer搭建的深度神经网络,可以实现高效的语义理解和文本生成能力。
- 改进的Transformer多头注意力MHA架构,提出了多头潜在注意力MLA,实现对K,V向量的低秩分解,降低了训练和推理中的算力需求。
- 混合专家结构,DeepSeek大模型由多个小型专家网络模型组成,不同的专家网络为不同的场景领域进行优化训练,在接受任务时根据任务类型自动路由到特定的专家网络进行推理,有效节省了对算力的需求。DeepSeek大模型全参数版本由6710亿参数,但推理时只需要用到特定专家网络的370亿参数。
- 强化学习技术,通过在V3大模型上采用改进后的GPRO优化算法以及基于规则的奖励系统进行强化学习,可以有效地训练模型使用长思维链方式来增加其推理思考能力。
- 知识蒸馏技术,把R1大模型生成的预料数据,对其他较小参数量的大模型如LLAMA或阿里通义千问进行知识蒸馏训练,有效提高了这些大模型的性能,实现了在相同参数量技术上对原有性能的提高。
部署过程是,首先要在华为官网下载相应的MindIE镜像,这个网页有产品版本信息-版本说明-MindIE1.0.0开发文档-昇腾社区相关的介绍,在模型库-ModelZoo-昇腾这个网站上选择我们需要的模型,然后会介绍相应的镜像版本。对于DeepSeek-R1-Distill-Qwen系列的模型,对应910显卡的是1.0.0-800I-A2-py311-openeuler24.03-lts这个镜像版本。需要注意的是,这个镜像有ARM64和X86_64两个版本,因为我需要现在本地笔记本电脑下载镜像再上传到服务器,所以需要先在服务器上通过命令uname -m来查看服务器的架构,如果是ARM64的架构,在本地笔记本下载镜像的时候需要在docker pull后面加上--platform=linux/aarch64。
下一步是下载模型的权重文件,我们可以在hf-mirror.com这个网站上找到对应的权重文件,下载到本地。我选择的是DeepSeek-R1-Distill-Qwen-32B的模型。
然后在服务器端我们可以加载镜像运行容器了,例如以下的命令
docker run -it -d --net=host --shm-size=1g --network=llm-network -p 9999:1025 --privileged --name deepseek-qwen-32b --device=/dev/davinci_manager --device=/dev/hisi_hdc --device=/dev/devmm_svm -v /usr/local/Ascend/driver:/usr/local/Ascend/driver:ro -v /usr/local/sbin:/usr/local/sbin:ro -v /data/models/DeepSeek-R1-Distill-Qwen-32B:/model:rw swr.cn-south-1.myhuaweicloud.com/ascendhub/mindie:1.0.0-800I-A2-py311-openeuler24.03-lts bash
容器启动后,我们通过命令登录到容器上,修改Config.json配置文件。
docker exec -it deepseek-qwen-32b bash
chmod 750 /model
vim /usr/local/Ascend/mindie/latest/mindie-service/conf/config.json
修改以下位置
{
...
"ServerConfig" :
{
"ipAddress" : "0.0.0.0",
"allowAllZeroIpListening" : true,
"httpsEnabled" : false,
...
}
"BackendConfig" : {
"npuDeviceIds" : [[0,1]],
"ModelDeployConfig" :
{
"maxSeqLen" : 8192,
"maxInputTokenLen" : 4096,
"truncation" : false,
"ModelConfig" : [
{
"modelInstanceType" : "Standard",
"modelName" : "DeepSeek-R1-Distill-Qwen-32B",
"modelWeightPath" : "/model",
"worldSize" : 2,
"cpuMemSize" : 5,
"npuMemSize" : -1,
"backendType" : "atb",
"trustRemoteCode" : false
}
]
},
"ScheduleConfig" :
{
...
"maxIterTimes" : 8192,
}
}
}
在以上配置中,我设置了模型部署在2张910 NPU卡上,然后调整了maxSeqLen,maxInputTokenLen,maxIterTimes这几个参数。原来的默认值maxSeqLen=2560,maxInputTokenLen=2048,我理解对应的意思上输入Token不能超过2048,然后输出Token不能超过2560-2048=512个,如果要调大输出的token数,也要相应做出调整,但是好像maxIterTimes这个参数也要做相应调整,按照文档意思这是控制一句话的最大长度的。
最后就是启动推理服务,提供服务化API接口给外部应用调用。
cd /usr/local/Ascend/mindie/latest/mindie-service/bin
./mindieservice_daemon
API接口提供了兼容OpenAI的格式,所以服务启动后我们可以调用接口测试一下
curl -X POST "http://127.0.0.1:1025/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"model": "DeepSeek-R1-Distill-Qwen-32B",
"max_tokens": 3000,
"stream": false,
"temperature": 0.6,
"messages": [
{"role": "user", "content": "Please describe guangzhou city in about 2000 words"}
]
}'
注意这里设置了Temperature为0.6,以及在Message里面没有设置System Prompt,这是根据官网上的说明来设置的:
- Set the temperature within the range of 0.5-0.7 (0.6 is recommended) to prevent endless repetitions or incoherent outputs.
- Avoid adding a system prompt; all instructions should be contained within the user prompt.
向量嵌入模型的部署
现在要部署一个向量嵌入模型来配合知识库的使用。在MindIE的网站提到了可以适配HuggingFace的Text Embeddings Interface框架,见网页功能介绍-TEI-MindIE开源第三方服务化框架适配开发指南-服务化集成部署-MindIE1.0.0开发文档-昇腾社区
但是这个TEI的接口和OpenAI或或者Ollama的embedding接口不兼容,因为我想部署Open Webui来对接,Open webui无法调用TEI,因此我还不能直接采取部署TEI镜像的方式来启动Embedding服务。
在MindIE镜像的/usr/local/Ascend/atb-models/examples/models目录下,有介绍当前适配的一些模型,其中我们可以找到Bge这个目录,这是对应bge-zh这个embedding模型的。因此我尝试加载这个模型。同样在hf-mirror网站上找到bge-large-zh-v1.5模型的权重并下载,然后启动一个新的容器,如以下命令
docker run -it -d --net=host --shm-size=1g --network=llm-network -p 9998:8088 --privileged --name bge-zh --device=/dev/davinci_manager --device=/dev/hisi_hdc --device=/dev/devmm_svm -v /usr/local/Ascend/driver:/usr/local/Ascend/driver:ro -v /usr/local/sbin:/usr/local/sbin:ro -v /data/models/bge-base-zh-v1.5:/model:rw swr.cn-south-1.myhuaweicloud.com/ascendhub/mindie:1.0.0-800I-A2-py311-openeuler24.03-lts bash
容器启动后,进入容器
docker exec -it bge-zh bash
chmod 750 /model
source /usr/local/Ascend/ascend-toolkit/set_env.sh
下载的模型权重都放置在容器的/model目录下,然后调用模型的脚本文件是放置在容器的/usr/local/Ascend/atb-models/examples/models/embeddings目录下,参考/model/config.json中的model_type的值,用脚本文件目录/usr/local/Ascend/atb-models/examples/models/embeddings/bert的'modeling_bert.py`替换下载模型权重的 `modeling_bert.py'
修改模型权重配置文件/model/config.json,修改_name_or_path的值指向/model,设置auto_map的值,修改后的配置如下:
{
"_name_or_path": "/model",
"auto_map": {
"AutoModel": "/model--modeling_bert.BertModel"
},
...
}
完成后即可在脚本文件目录下运行以下命令来测试
python run.py \
embed \
--model_name_or_path=/model \
--trust_remote_code \
--device_type=npu \
--device_id=3 \
--text='this is a test'
可以看到能成功输出Embedding后的向量。
之后就是增加一个和Ollama兼容的embed接口,这里我采用fastapi来做,新建一个名为embed_api.py文件,内容如下:
import time
from typing import Any, List, Union
import torch
from transformers.tokenization_utils_base import BatchEncoding
from atb_llm.utils.log.logging import logger, message_filter
from atb_llm.utils.log.error_code import ErrorCode
from model_runner import ModelRunner, Arguments
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
import uvicorn
from pydantic import BaseModel
app = FastAPI()
class Runner:
def __init__(self, **kwargs: Any) -> None:
self.model = ModelRunner(
str(kwargs.pop("model_name_or_path", None)),
bool(kwargs.pop("trust_remote_code", True)),
getattr(torch, kwargs.pop("torch_dtype", "float16")),
torch.device(f"{kwargs.pop('device_type', 'cpu')}:{str(kwargs.pop('device_id', 0))}"),
str(kwargs.pop("model_type", None))
)
self.model_name_or_path = self.model.model_name_or_path
self.torch_dtype = self.model.torch_dtype
self.device = self.model.device
#self.device = 'npu:4'
self.model_type = self.model.model_type
self.config = self.model.config
self.tokenizer = self.model.tokenizer
self.max_batch_size = kwargs.pop("max_batch_size", 1)
self.max_seq_len = kwargs.pop("max_seq_len", self.tokenizer.model_max_length)
self.return_tensors = kwargs.pop("return_tensors", "pt")
self.tokenizer_args = {
"padding": kwargs.pop("padding", "max_length"),
"truncation": kwargs.pop("truncation", True),
"return_tensors": self.return_tensors,
"max_length": self.max_seq_len,
}
def warm_up(self) -> None:
inputs = self.model.generate_inputs(
self.tokenizer.model_input_names,
self.tokenizer.vocab_size,
self.max_batch_size,
self.max_seq_len,
self.device
)
encoded_inputs = BatchEncoding(inputs, tensor_type=self.return_tensors)
logger.info("---------------begin warm_up---------------")
tick = time.perf_counter()
model_outputs = self.model.forward(encoded_inputs)
torch.nn.functional.normalize(model_outputs[0]).view(-1, ).float().cpu()
tock = time.perf_counter()
logger.info("---------------end warm_up---------------")
logger.info(f"warm time: {(tock - tick) * 1000:.2f} ms")
def embed(self, texts: Union[str, List[str], List[List[str]]]) -> torch.Tensor:
encoded_inputs = self.model.tokenize(texts, **self.tokenizer_args)
logger.info("---------------begin embed---------------")
tick = time.perf_counter()
embeddings = self.model.embed(encoded_inputs)
tock = time.perf_counter()
logger.info("---------------end embed---------------")
logger.info(f"embed time: {(tock - tick) * 1000:.2f} ms")
return embeddings
def rerank(self, texts: Union[str, List[str], List[List[str]]]) -> torch.Tensor:
encoded_inputs = self.model.tokenize(texts, **self.tokenizer_args)
logger.info("---------------begin rerank---------------")
tick = time.perf_counter()
scores = self.model.rerank(encoded_inputs)
tock = time.perf_counter()
logger.info("---------------end rerank---------------")
logger.info(f"rerank time: {(tock - tick) * 1000:.2f} ms")
return scores
class EmbedRequest(BaseModel):
model: str
input: Union[str, List[str]]
runner = None
@app.on_event("startup")
async def startup_event():
global runner
runner = Runner(
model_name_or_path='/model',
device_type='npu',
device_id=4,
trust_remote_code=True,
model_type='float',
torch_dtype='float16',
padding='max_length',
truncation=True,
return_tensors='pt',
request='embed'
)
@app.post("/embed")
async def embed_text(request: EmbedRequest):
results = runner.embed(request.input)
results_list = results.tolist()
json_compatible_data = jsonable_encoder(results_list)
return {
"embeddings": json_compatible_data
}
if __name__ == "__main__":
uvicorn.run("embed_api:app", host="0.0.0.0", port=8088, reload=True, log_level="info")
运行这个文件即可提供和Ollama兼容的embed API服务。
Open Webui的部署
最后可以部署一个Web服务,来提供对Deepseek服务的调用了。
Open WebUI 是一个开源的、功能丰富且用户友好的自托管 AI 平台,支持调用 Ollama 和 OpenAI 兼容的 API来使用大语言模型,并内置了用于检索增强生成(RAG)的推理引擎。
下载ARM64平台的Open webui镜像
docker pull ghcr.io/open-webui/open-webui:main --platform=linux/aarch64
在服务器建立一个open_webui的目录,并新建一个.env配置文件,内容如下
OPENAI_API_BASE_URL='http://deepseek-qwen-32b:1025/v1'
OPENAI_API_KEY=''
ENABLE_OLLAMA_API=false
HF_HUB_OFFLINE=1
然后通过命令启动
docker run -d -p 80:8080 --network=llm-network -v /data/open_webui:/app/backend/data -v /data/open_webui/.env:/app/.env --name open-webui ghcr.io/open-webui/open-webui:main
题外话:
因为服务器是在内网,需要通过Nginx转发才能访问Open webui服务,最开始调用Open webui始终有问题,页面加载后显示空白,看了一下浏览器开发者工具的日志,提示是_app目录下的js文件路径不对,看了一下open webui的代码,发现这些路径都是通过href='/_app/xxx.js'这种绝对路径的方式来写的,因此在Nginx下面需要配置一下路径改写。配置后可以成功访问页面,但是在聊天窗口调用Deepseek又出现问题,一直停留在等待回复的状态,看了一下日志,发现很多web socket的链接被关掉了,怀疑是因为Nginx没有配置http 1.1版本导致的,在Nginx中增加以下配置即可。
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";