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

posted @ 2025-03-13 10:57  cupwym  阅读(331)  评论(0)    收藏  举报