QwQ部署多卡
docker启动脚本
#!/bin/bash
set -x # 启用调试模式
export MY_CONTAINER="QwQ_env"
num=$(sudo docker ps -a | grep "$MY_CONTAINER" | wc -l)
echo $num
echo $MY_CONTAINER
if [ 0 -eq $num ]; then
sudo xhost +local:docker
export DISPLAY=$(echo $DISPLAY)
if [ -z "$DISPLAY" ]; then
export DISPLAY=:0
fi
sudo docker run \
--gpus all \
--net=host \
--pid=host \
--shm-size=256G \# 设置为 256GB 共享内存
-e DISPLAY=$DISPLAY \
-v /home/cas/cupming/Qwen:/workspace/Qwen \
-p 10086:10086 \
-p 8000:8000 \
--ulimit memlock=-1:-1 \# 取消内存锁定限制
--ulimit stack=536870912 \# 设置线程的最大栈大小为512MB
--ipc=host \# 允许共享主机的共享内存
-it \
--privileged \
--name $MY_CONTAINER \
-w /workspace/Qwen \
nvcr.io/nvidia/pytorch:25.02-py3 \
/bin/bash
else
sudo docker start $MY_CONTAINER
sudo docker exec -w /workspace/Qwen -ti $MY_CONTAINER /bin/bash
fi
环境:注意vllm使用自定义flashattention,如果报错卸载环境中的
python -m pip install --upgrade pip
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install modelscope==1.22.3
pip install openai==1.61.0
pip install tqdm==4.67.1
pip install transformers==4.48.2
pip install vllm==0.7.1
pip install streamlit==1.41.1
python测试脚本单gpu
新建 vllm_model.py 文件并在其中输入下文的代码:
首先从 vLLM 库中导入 LLM 和 SamplingParams 类。LLM 类是使用 vLLM 引擎运行离线推理的主要类。SamplingParams 类指定采样过程的参数,用于控制和调整生成文本的随机性和多样性。
vLLM 提供了非常方便的封装,我们直接传入模型名称或模型路径即可,不必手动初始化模型和分词器。
# vllm_model.py
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
import os
import json
# 自动下载模型时,指定使用modelscope; 否则,会从HuggingFace下载
os.environ['VLLM_USE_MODELSCOPE']='True'
def get_completion(prompts, model, tokenizer=None, max_tokens=8192, temperature=0.6, top_p=0.95, max_model_len=2048):
stop_token_ids = [151329, 151336, 151338]
# 创建采样参数。temperature 控制生成文本的多样性,top_p 控制核心采样的概率,避免无休止的重复
sampling_params = SamplingParams(temperature=temperature, top_p=top_p, max_tokens=max_tokens, stop_token_ids=stop_token_ids)
# 初始化 vLLM 推理引擎
llm = LLM(model=model, tokenizer=tokenizer, max_model_len=max_model_len,trust_remote_code=True)
outputs = llm.generate(prompts, sampling_params)
return outputs
if __name__ == "__main__":
# 初始化 vLLM 推理引擎
model='/workspace/Qwen/QwQ-32B' # 指定模型路径
# model='/root/autodl-tmp/Qwen/QwQ-32B-AWQ' # 指定模型名称,自动加载模型
tokenizer = None
# 加载分词器后传入vLLM 模型,但不是必要的。
# tokenizer = AutoTokenizer.from_pretrained(model, use_fast=False)
text = ["9.11与9.9哪个更大", ] # 可用 List 同时传入多个 prompt,根据 qwen 官方的建议,每个 prompt 都需要以 <think>\n 结尾,如果是数学推理内容,建议包含(中英文皆可):Please reason step by step, and put your final answer within \boxed{}.
# messages = [
# {"role": "user", "content": prompt+"<think>\n"}
# ]
# 作为聊天模板的消息,不是必要的。
# text = tokenizer.apply_chat_template(
# messages,
# tokenize=False,
# add_generation_prompt=True
# )
outputs = get_completion(text, model, tokenizer=tokenizer, max_tokens=8192, temperature=0.6, top_p=0.95, max_model_len=2048) # 思考需要输出更多的 Token 数,max_tokens 设为 8K,根据 qwen 官方的建议,temperature应在 0.5-0.7,推荐 0.6
# 输出是一个包含 prompt、生成文本和其他信息的 RequestOutput 对象列表。
# 打印输出。
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
if r"</think>" in generated_text:
think_content, answer_content = generated_text.split(r"</think>")
else:
think_content = ""
answer_content = generated_text
print(f"Prompt: {prompt!r}, Think: {think_content!r}, Answer: {answer_content!r}")
python测试脚本多gpu
# vllm_model_mulgpus.py
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
import os
import json
# 如果不需要从 ModelScope 或 Hugging Face 下载模型,可以不设置此环境变量
# os.environ['VLLM_USE_MODELSCOPE'] = 'True'
def get_completion(prompts, model, tokenizer=None, max_tokens=8192, temperature=0.6, top_p=0.95, max_model_len=2048, tensor_parallel_size=1):
stop_token_ids = [151329, 151336, 151338]
sampling_params = SamplingParams(temperature=temperature, top_p=top_p, max_tokens=max_tokens, stop_token_ids=stop_token_ids)
# 初始化 vLLM 推理引擎,确保加载本地模型
llm = LLM(
model=model, # 本地模型路径
tokenizer=tokenizer, # 可选的分词器
max_model_len=max_model_len, # 最大模型长度
trust_remote_code=True, # 信任远程代码(如果模型需要)
tensor_parallel_size=tensor_parallel_size, # 多卡设置
distributed_executor_backend="mp", # 单节点多卡使用 multiprocessing
enforce_eager=True, # 强制本地加载,避免 CUDA 图优化干扰
download_dir=None, # 不从远程下载,直接使用本地路径
dtype="auto" # 自动检测数据类型
)
outputs = llm.generate(prompts, sampling_params)
return outputs
if __name__ == "__main__":
# 指定本地模型路径(确保路径拼写正确)
model = '/workspace/Qwen/QwQ-32B' # 注意路径拼写,之前是 /worksapce,可能是笔误
tokenizer = None
# 可选:从本地加载分词器
# tokenizer = AutoTokenizer.from_pretrained(model, use_fast=False, local_files_only=True)
text = ["9.11与9.9哪个更大", ]
num_gpus = 4 # 多卡设置
outputs = get_completion(text, model, tokenizer=tokenizer, tensor_parallel_size=num_gpus)
for prompt, output in zip(text, outputs):
generated_text = output.outputs[0].text.strip()
if r"</think>" in generated_text:
think_content, answer_content = generated_text.split(r"</think>")
else:
think_content = ""
answer_content = generated_text
print(f"Prompt: {prompt!r}, Think: {think_content!r}, Answer: {answer_content!r}")
创建兼容 OpenAI API 接口的服务器
vLLM 兼容 OpenAI API 协议,所以我们可以直接使用 vLLM 创建 OpenAI API 服务器。vLLM 部署实现 OpenAI API 协议的服务器非常方便。默认会在 http://localhost:8000 启动服务器。服务器当前一次托管一个模型,并实现列表模型、completions 和 chat completions 端口。
-
completions:是基本的文本生成任务,模型会在给定的提示后生成一段文本。这种类型的任务通常用于生成文章、故事、邮件等。
-
chat completions:是面向对话的任务,模型需要理解和生成对话。这种类型的任务通常用于构建聊天机器人或者对话系统。
在创建服务器时,我们可以指定模型名称、模型路径、聊天模板等参数。 -
--host 和 --port 参数指定地址。
-
--model 参数指定模型名称。
-
--chat-template 参数指定聊天模板。
-
--served-model-name 指定服务模型的名称。
-
--max-model-len 指定模型的最大长度。
复制以下代码到命令行运行:vllm serve /workspace/Qwen/QwQ-32B \ --tensor-parallel-size 4 \ --gpu-memory-utilization 0.90 \-
新建一个命令行界面
-
通过 curl 命令查看当前的模型列表
curl http://localhost:8000/v1/models
-
使用 curl 命令测试 OpenAI Completions API
curl http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "/workspace/Qwen/QwQ-32B",
"prompt": "10的阶乘是多少?<think>\n",
"max_tokens": 1024,
"temperature": 0
}'
用 Python 脚本请求 OpenAI Completions API
# vllm_openai_completions.py
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="sk-xxx", # 随便填写,只是为了通过接口参数校验
)
completion = client.chat.completions.create(
model="/workspace/Qwen/QwQ-32B",
messages=[
{"role": "user", "content": "10的阶乘是多少?<think>\n"}
]
)
print(completion.choices[0].message)
创建前端webdemo进行本地交互
#app.py
import streamlit as st
import requests
import re
# 在侧边栏中创建一个标题和一个链接
with st.sidebar:
st.markdown("## QwQ-32B LLM")
# 创建一个滑块,用于选择最大长度,范围在 0 到 8192 之间,默认值为 4096(QwQ-32B 支持 8192 tokens,不过我们一张4090显存较小,稳妥起见设置最大长度为为2048)
max_length = st.slider("max_length", 0, 8192, 4096, step=1)
# 创建一个标题和一个副标题
st.title("💬 QwQ-32B Chatbot")
st.caption("🚀 A streamlit chatbot powered by Self-LLM")
# 文本分割函数
def split_text(text):
pattern = re.compile(r'<think>(.*?)</think>(.*)', re.DOTALL) # 定义正则表达式模式
match = pattern.search(text) # 匹配 <think>思考过程</think>回答
if match: # 如果匹配到思考过程
think_content = match.group(1).strip() # 获取思考过程
answer_content = match.group(2).strip() # 获取回答
else:
think_content = "" # 如果没有匹配到思考过程,则设置为空字符串
answer_content = text.strip() # 直接返回回答
return think_content, answer_content
# 如果 session_state 中没有 "messages",则创建一个包含默认消息的列表
if "messages" not in st.session_state:
st.session_state["messages"] = [{"role": "assistant", "content": "有什么可以帮您的?"}]
# 遍历 session_state 中的所有消息,并显示在聊天界面上
for msg in st.session_state.messages:
st.chat_message(msg["role"]).write(msg["content"])
# 如果用户在聊天输入框中输入了内容,则执行以下操作
if prompt := st.chat_input():
# 在聊天界面上显示用户的输入
st.chat_message("user").write(prompt)
# 将用户输入添加到 session_state 中的 messages 列表中
st.session_state.messages.append({"role": "user", "content": prompt})
# 调用本地运行的 vllm 服务
try:
response = requests.post(
"http://localhost:8000/v1/chat/completions",
json={
"model": "/workspace/Qwen/QwQ-32B",
"messages": st.session_state.messages,
"max_tokens": max_length
}
)
if response.status_code == 200:
response_data = response.json()
assistant_response = response_data["choices"][0]["message"]["content"]
think_content, answer_content = split_text(assistant_response) # 调用split_text函数,分割思考过程和回答
# 将模型的输出添加到 session_state 中的 messages 列表中
st.session_state.messages.append({"role": "assistant", "content": assistant_response})
# 在聊天界面上显示模型的输出
with st.expander("模型思考过程"):
st.write(think_content) # 展示模型思考过程
st.chat_message("assistant").write(answer_content) # 输出模型回答
else:
st.error("Error generating response.")
except Exception as e:
st.error(f"An error occurred: {e}")
执行streamlit run app.py 打开http://localhost:8501 即可在网页与本地模型对话了。(--server.address 127.0.0.1 --server.port 6006)可选
流式处理返回webdemo
import streamlit as st
import requests
import re
import json
# 在侧边栏中创建一个标题和一个链接
with st.sidebar:
st.markdown("## QwQ-32B LLM")
# 创建一个滑块,用于选择最大长度,范围在 0 到 8192 之间,默认值为 4096(QwQ-32B 支持 8192 tokens,不过我们一张4090显存较小,稳妥起见设置最大长度为为2048)
max_length = st.slider("max_length", 0, 8192, 4096, step=1)
# 创建一个标题和一个副标题
st.title("💬 QwQ-32B Chatbot")
st.caption("🚀 A streamlit chatbot powered by Self-LLM")
# 文本分割函数
def split_text(text):
pattern = re.compile(r'<think>(.*?)</think>(.*)', re.DOTALL) # 定义正则表达式模式
match = pattern.search(text) # 匹配 <think>思考过程</think>回答
if match: # 如果匹配到思考过程
think_content = match.group(1).strip() # 获取思考过程
answer_content = match.group(2).strip() # 获取回答
else:
think_content = "" # 如果没有匹配到思考过程,则设置为空字符串
answer_content = text.strip() # 直接返回回答
return think_content, answer_content
# 如果 session_state 中没有 "messages",则创建一个包含默认消息的列表
if "messages" not in st.session_state:
st.session_state["messages"] = [{"role": "assistant", "content": "有什么可以帮您的?"}]
# 遍历 session_state 中的所有消息,并显示在聊天界面上
for msg in st.session_state.messages:
st.chat_message(msg["role"]).write(msg["content"])
# 如果用户在聊天输入框中输入了内容,则执行以下操作
if prompt := st.chat_input():
# 在聊天界面上显示用户的输入
st.chat_message("user").write(prompt)
# 将用户输入添加到 session_state 中的 messages 列表中
st.session_state.messages.append({"role": "user", "content": prompt})
# 创建空容器用于流式显示
thinking_container = st.empty()
message_container = st.chat_message("assistant")
response_container = message_container.empty()
# 调用本地运行的 vllm 服务
try:
response = requests.post(
"http://localhost:8000/v1/chat/completions",
json={
"model": "/workspace/Qwen/QwQ-32B",
"messages": st.session_state.messages,
"max_tokens": max_length,
"stream": True # 启用流式输出
},
stream=True # 启用 requests 的流式响应
)
if response.status_code == 200:
full_response = ""
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
line = line[6:] # 删除 "data: " 前缀
if line != '[DONE]':
chunk_data = json.loads(line)
if chunk_data['choices'][0]['finish_reason'] is None:
chunk = chunk_data['choices'][0]['delta'].get('content', '')
full_response += chunk
# 逐步更新显示的内容
think_content, answer_content = split_text(full_response)
if think_content:
thinking_container.markdown(f"**思考过程:**\n{think_content}")
response_container.markdown(answer_content)
# 将完整响应添加到会话状态
st.session_state.messages.append({"role": "assistant", "content": full_response})
else:
st.error("Error generating response.")
except Exception as e:
st.error(f"An error occurred: {e}")
启动 vLLM 集群:将模型下载好,分别放入两台服务器的目录中,注:两台服务器的模型存放目录路径务必保持一致
下载启动集群脚本 run_cluster.sh
vllm/examples/online_serving/run_cluster.sh at main · vllm-project/vllm
wget https://github.com/vllm-project/vllm/blob/main/examples/online_serving/run_cluster.sh
1、启动 head 主节点
# 可以配合nohup后台运行
# 为何指定版本,可下拉看【常见问题】章节
sudo bash run_cluster.sh \
vllm/vllm-openai:v0.7.3 \
主节点IP \
--head \
/模型目录 \
-e VLLM_HOST_IP=当前机器的ip
2、启动worker从节点
# 可以配合nohup后台运行
# 为何指定版本,可下拉看【常见问题】章节
sudo bash run_cluster.sh \
vllm/vllm-openai:v0.7.3 \
主节点IP \
--worker \
/模型目录 \
-e VLLM_HOST_IP=当前机器的ip
3、进入head容器内启动推理服务
# 使用docker ps命令查找容器id
sudo docker ps
# 进入容器内部
sudo docker exec -it 容器ID bash
## 启动推理服务
# 容器内模型目录固定为 /root/.cache/huggingface/ 开头,该目录指向为 run_cluster.sh 参数中配置的映射的模型目录
vllm serve /root/.cache/huggingface/gguf/DeepSeek-R1-Distill-Qwen-32B-Q3_K_L.gguf \
# gguf 量化模型建议使用官方原生的 tokenizer
--tokenizer /root/.cache/huggingface/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B \
# 模型名称,用于接口中指定模型
--served-model-name DeepSeek-Qwen-32B-Q3-K-L \
# 暴露的 api 端口
--port 8000 \
# 占用的最大显卡内存比例, 0.9 则为 90%
--gpu-memory-utilization 0.90 \
# 最大模型上下文长度,
--max-model-len=16384 \
# 此处填单台服务器的显卡数量,本文单台服务器有 2 张显卡
--tensor-parallel-size 2 \
# 此处填服务器数量,本为为 2 台服务器
--pipeline-parallel-size 2
常见问题
报错:Gloo connectFullMesh failed
如下图,使用 ifconfig 命令列出服务器网卡,找到用于内网通讯的网卡名,比如我这里的名称为ens3
修改启动 vLLM 集群的命令,注: head 节点和 worker 节点的启动命令都需要修改!
# 启动主节点
sudo bash run_cluster.sh \
vllm/vllm-openai \
主节点IP \
--head \
/模型目录 \
-e VLLM_HOST_IP=当前机器的ip \
# 增加下面这一行,指定网卡名称
-e GLOO_SOCKET_IFNAME=ens3
## 启动从节点
sudo bash run_cluster.sh \
vllm/vllm-openai \
主节点IP \
--worker \
/模型目录 \
-e VLLM_HOST_IP=当前机器的ip
# 增加下面这一行,指定网卡名称
-e GLOO_SOCKET_IFNAME=ens3
浙公网安备 33010602011771号