跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PythonAI算法

基于 Docker 和 Ray 的 vLLM 分布式推理部署:Qwen3-4B 在双 3090 环境

在 Ubuntu 24.04 环境下,利用 Docker 和 NVIDIA Container Toolkit 构建双容器分布式集群,结合 Ray 调度框架与 vLLM 推理引擎,在两张 RTX 3090 显卡上部署并运行 Qwen3-4B-Instruct 大模型的完整流程。内容涵盖镜像构建、集群启动、模型下载、分布式服务配置及交互测试脚本,旨在实现单卡独占资源下的多卡协同推理,提升吞吐量与并发处理能力。

DotNetGuy发布于 2026/2/9更新于 2026/7/2357 浏览
基于 Docker 和 Ray 的 vLLM 分布式推理部署:Qwen3-4B 在双 3090 环境

零 实验软硬件环境说明

分类项目配置详情备注
硬件环境CPUe5 2696V3(逻辑核心数>=16)启动时分配 8 cores 给 Ray
物理内存 (RAM)32 GB关键瓶颈,需要精细规划共享内存
GPUNVIDIA GeForce RTX 3090 x 2单卡 24GB VRAM
网络无线网卡 (USB)wlxe0ad47220334
操作系统OS 发行版Ubuntu 24.04 LTS内核版本 6.14.0-37-generic
宿主机 IP10.193.195.59(机器正常活动网卡)无线网络 IP,作为集群通信地址
基础软件Docker 管理Docker Engine开源容器引擎
NVIDIA 驱动570.211.01支持 CUDA 12.8
Docker 网络Host 模式 (--network host)容器与宿主机共享网络栈,性能最优
核心环境基础镜像ray-vllm:3090-cuda12.8包含 Ray, vLLM, CUDA 12.8
Python3.12.3Ray 脚本路径显示
集群配置Ray 版本2.53.0支持 ray metrics launch-prometheus
集群架构Head Node + Worker Node (双容器)ray-node-0 (Head), ray-node-1 (Worker)
通信端口6379 (GCS), 8265 (Dashboard), 8080 (Metrics)确保 6379 未被 Redis 占用

目录

  1. 零 实验软硬件环境说明
  2. 一 核心架构
  3. 二 Docker 和 NVIDIA Container Toolkit 准备
  4. 2.1 Docker 环境准备
  5. 示例镜像代理配置
  6. 2.2 NVIDIA Container Toolkit 环境准备
  7. 1 安装依赖
  8. 2 添加密钥和仓库
  9. 3 执行安装
  10. 4 更新 Docker 配置
  11. 5 重启 Docker 服务
  12. 三 定制化 Docker 镜像
  13. 3.1 构建镜像存放位置
  14. 3.2 创建 Dockerfile 文件
  15. 基础镜像:CUDA 12.8.1 + CUDNN+ Ubuntu 24.04(适配 3090,兼容 vLLM/Ray)
  16. 第一步:修复 NVIDIA GPG 密钥警告,刷新 apt 缓存
  17. 第二步:安装基础依赖
  18. 第三步:直接配置国内 PyPI 源
  19. 第四步:安装 Ray(分布式核心,2.53.0 稳定版,兼容 Python3.11/CUDA 12.8)
  20. 第五步:安装 vLLM(大模型推理,适配 CUDA 12.8,添加--break-system-packages 解决 ubuntu24.4 报错)
  21. 第六步:安装 PyTorch(适配 CUDA 12.8,使用官方 whl 包)
  22. 第七步:设置 CUDA 12.8 环境变量,确保依赖能找到 CUDA 路径
  23. 工作目录
  24. 3.3 构建 Docker 镜像
  25. 3.4 启动双 Docker 容器(单容器单 3090,桥接网络)
  26. 容器 1 - 使用第一张卡 (GPU 0)
  27. 容器 2 - 使用第二张卡 (GPU 1)
  28. 3.5 验证容器状态
  29. 在容器内重新设置可用 GPU 变量才生效
  30. 在容器内重新设置可用 GPU 变量才生效
  31. 四 搭建 Ray 分布式集群
  32. 4.1 确定宿主机 IP(集群通信地址)
  33. 4.2 主节点启动 Ray Head 节点
  34. 进入主节点容器
  35. 启动 Ray Head 节点
  36. 4.3 从节点容器加入 Ray 集群
  37. 进入从节点容器
  38. 启动 Ray 从节点
  39. 4.4 验证 Ray 集群状态
  40. 进入主节点容器
  41. 查看集群节点
  42. 输出:2 个节点(head+worker),每个节点 1 个 GPU,总计 2 个 GPU/32 个 CPU
  43. 4.5 查看 Ray 仪表盘
  44. 4.6 节点内验证 Ray 集群状态
  45. 连接到 Ray 集群
  46. 获取集群资源
  47. 获取所有节点
  48. 断开连接
  49. 五 模型下载和运行
  50. 5.1 下载 Qwen3-4B 模型
  51. 5.2 Ray+vLLM 分布式运行 Qwen3-4B 大模型
  52. 5.3 查看 GPU 运行状态
  53. 六 交互式分布式推理(快速测试)
  54. 6.1 脚本测试
  55. 逐块打印生成内容
  56. 每个 chunk 里可能有 choices[0].delta.content
  57. 6.2 网页对话交互测试
  • 免费图片AI生成工具免费生成了解详情
资源分配GPU 策略独占模式--gpus all + NVIDIA_VISIBLE_DEVICES=0/1
共享内存12 GB / 容器物理内存 32GB 下的折中方案
内存优化Memfd 启用环境变量 VLLM_USE_MEMFD=1 绕过 /dev/shm 限制

一 核心架构

  • 宿主机(Ubuntu24.04+ 双 3090)部署2 个 Docker 容器,单容器独占 1 张 3090,通过--gpus device=N精准绑定显卡;容器内搭建 Ray 分布式集群(1 主 1 从),基于 vLLM 实现 4B 大模型的分布式推理/部署,全程保留宿主机 3090 的可视化使用,无需 GPU 直通,兼顾开发操作与算力利用。
  • 适配性:单张 3090(24G 显存)满足 8B 模型(FP16 约 16G/INT4 约 4G)的显存要求,vLLM 的 PagedAttention 技术进一步降低显存占用,双容器分布式可提升推理吞吐量/并发量。

前置条件

  1. 宿主机已装Ubuntu24.04 图形版,双 3090 显卡正常识别,开启基础虚拟化(BIOS 的 VMX)。
  2. 宿主机已装NVIDIA 官方驱动(≥535,建议 545/550),验证:nvidia-smi能正常显示双 3090。
  3. 宿主机网络正常,能拉取 Docker 镜像,建议换国内镜像源。

二 Docker 和 NVIDIA Container Toolkit 准备

2.1 Docker 环境准备

  1. 确保宿主机已安装 Docker。若未安装,请通过官方渠道或包管理器安装 Docker Engine。
  2. 可以在 Docker 配置中添加镜像代理以加速拉取。
# 示例镜像代理配置
https://docker.1ms.run https://swr.cn-north-4.myhuaweicloud.com 
  1. 拉取 NVIDIA CUDA 基础镜像。
docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04
docker tag swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04 docker.io/nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04 

2.2 NVIDIA Container Toolkit 环境准备

  • NVIDIA Container Toolkit 的配置主要参考 NVIDIA 官方文档。
# 1 安装依赖
sudo apt-get update
sudo apt-get install -y curl

# 2 添加密钥和仓库
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# 3 执行安装
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# 4 更新 Docker 配置
sudo nvidia-ctk runtime configure --runtime=docker

# 5 重启 Docker 服务
sudo systemctl restart docker

三 定制化 Docker 镜像

  • 基于nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04基础镜像(适配 3090,兼容 Ubuntu24.04),构建包含 Python3.10+Ray+vLLM + 依赖库的定制镜像,确保双容器环境一致,避免版本冲突。

3.1 构建镜像存放位置

mkdir -p /opt/ray-vllm && cd /opt/ray-vllm

3.2 创建 Dockerfile 文件

cd /opt/ray-vllm
vim Dockerfile
# 基础镜像:CUDA 12.8.1 + CUDNN+ Ubuntu 24.04(适配 3090,兼容 vLLM/Ray)
FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04

# 第一步:修复 NVIDIA GPG 密钥警告,刷新 apt 缓存
RUN mkdir -p /etc/apt/trusted.gpg.d &&\
cp /etc/apt/trusted.gpg /etc/apt/trusted.gpg.d/nvidia-cuda.gpg &&\
apt update &&\
apt clean &&\
rm -rf /var/lib/apt/lists/*

# 第二步:安装基础依赖
RUN apt update && apt install -y --no-install-recommends \
 python3-dev python3-pip \
git wget vim net-tools iputils-ping \
&& rm -rf /var/lib/apt/lists/*

# 第三步:直接配置国内 PyPI 源
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

# 第四步:安装 Ray(分布式核心,2.53.0 稳定版,兼容 Python3.11/CUDA 12.8)
RUN pip install ray[default,serve]==2.53.0 -i https://pypi.tuna.tsinghua.edu.cn/simple --break-system-packages

# 第五步:安装 vLLM(大模型推理,适配 CUDA 12.8,添加--break-system-packages 解决 ubuntu24.4 报错)
RUN pip install vllm[all] -i https://pypi.tuna.tsinghua.edu.cn/simple --break-system-packages

# 第六步:安装 PyTorch(适配 CUDA 12.8,使用官方 whl 包)
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128 --break-system-packages &&\
 pip install transformers accelerate sentencepiece -i https://pypi.tuna.tsinghua.edu.cn/simple --break-system-packages

# 第七步:设置 CUDA 12.8 环境变量,确保依赖能找到 CUDA 路径
ENV CUDA_HOME=/usr/local/cuda-12.8
ENV PATH=$CUDA_HOME/bin:$PATH
ENV LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH

# 工作目录
WORKDIR /root/ray-vllm

3.3 构建 Docker 镜像

  • 进入 Dockerfile 所在目录,执行构建命令,镜像命名为 ray-vllm:3090-cuda12.8。构建时间约 10 分钟,全程无报错即成功。
cd /opt/ray-vllm
docker build -t ray-vllm:3090-cuda12.8 .

3.4 启动双 Docker 容器(单容器单 3090,桥接网络)

  • 创建2 个容器,分别绑定3090-0(第 1 张)和 3090-1(第 2 张)显卡,使用桥接网络(容器与宿主机/容器间同一网段,Ray 集群通信必备),同时映射端口(Ray 仪表盘/LLM 推理端口)。
  • 启动主节点容器(ray-node-0)绑定 GPU 0;启动从节点容器(ray-node-1)绑定 GPU 1。执行以下命令,创建并启动主从节点容器,分别独占 3090-0 和 3090-1,桥接网络,映射 Ray(6379/8265)和 vLLM(8000)端口。
# 容器 1 - 使用第一张卡 (GPU 0)
docker run -itd \
 --name ray-node-0 \
 --gpus device=0\
 --network host\
 --privileged \
 --shm-size=12g \
 -e VLLM_USE_MEMFD=1\
 -e NVIDIA_VISIBLE_DEVICES=0\
 -v /opt/ray-vllm/data-0:/root/ray-vllm/data \
 ray-vllm:3090-cuda12.8 /bin/bash

# 容器 2 - 使用第二张卡 (GPU 1)
docker run -itd \
 --name ray-node-1 \
 --gpus device=1\
 --network host\
 --privileged \
 --shm-size=12g \
 -e VLLM_USE_MEMFD=1\
 -e NVIDIA_VISIBLE_DEVICES=1\
 -v /opt/ray-vllm/data-1:/root/ray-vllm/data \
 ray-vllm:3090-cuda12.8 /bin/bash

3.5 验证容器状态

  • 说明:由于 NVIDIA Container Toolkit 的特殊机制,在但容器内使用 nvidia-smi 命令可以查看到两个 GPU,这是正常现象,但通过env | grep NVIDIA可以验证可以使用的 GPU,重点以此来验证容器内 GPU 隔离的成功。
  • 主节点容器状态验证
docker exec -it ray-node-0 bash
# 在容器内重新设置可用 GPU 变量才生效
export CUDA_VISIBLE_DEVICES=0
python3 -c "import torch; print('Node0:', torch.cuda.device_count())"
env | grep NVIDIA
exit
(base) root@yang-server:/opt/ray-vllm# docker exec -it ray-node-0 bash
root@yang-server:~/ray-vllm# python3 -c "import torch; print('Node0:', torch.cuda.device_count())"
Node0: 2
root@yang-server:~/ray-vllm# export CUDA_VISIBLE_DEVICES=0
root@yang-server:~/ray-vllm# python3 -c "import torch; print('Node0:', torch.cuda.device_count())"
Node0: 1
root@yang-server:~/ray-vllm# env | grep NVIDIA
NVIDIA_VISIBLE_DEVICES=0
  • 从节点容器状态验证。
docker exec -it ray-node-1 bash
# 在容器内重新设置可用 GPU 变量才生效
export CUDA_VISIBLE_DEVICES=1
python3 -c "import torch; print('Node1:', torch.cuda.device_count())"
env | grep NVIDIA
exit
(base) root@yang-server:~# docker exec -it ray-node-1 bash
root@yang-server:~/ray-vllm# export CUDA_VISIBLE_DEVICES=1
root@yang-server:~/ray-vllm# python3 -c "import torch; print('Node1:', torch.cuda.device_count())"
Node1: 1
root@yang-server:~/ray-vllm# env | grep NVIDIA
NVIDIA_VISIBLE_DEVICES=1

四 搭建 Ray 分布式集群

  • 分布式集群为双容器(1 主 1 从),每个容器内只有一张 GPU 可以使用。

4.1 确定宿主机 IP(集群通信地址)

  1. 在宿主机终端执行 ip 地址查看命令。
ip addr
(base) root@yang-server:~# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host noprefixroute valid_lft forever preferred_lft forever
2: enp5s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000 link/ether 00:e0:21:9d:65:a0 brd ff:ff:ff:ff:ff:ff
3: wlxe0ad47220334: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether e0:ad:47:22:03:34 brd ff:ff:ff:ff:ff:ff inet 10.193.195.59/17 brd 10.193.255.255 scope global dynamic noprefixroute wlxe0ad47220334 valid_lft 75329sec preferred_lft 75329sec inet6 fe80::5076:b18f:9e57:fbdc/64 scope link noprefixroute valid_lft forever preferred_lft forever
  1. **10.193.195.59** (网卡 wlxe0ad47220334)
    • 状态:state UP (正在运行)
    • 类型:无线网卡 (以 wl 开头)
    • 理由:处于 UP 状态且拥有正常互联网 IP 地址的网卡。Ray 集群的节点之间需要通过这个 IP 进行通信。
  2. **192.168.x.x** (有线网卡 enp5s0)
    • 状态:state DOWN (已断开)
    • 理由:有线网卡目前没有连接网线或者没有获取到 IP,所以无法使用。

4.2 主节点启动 Ray Head 节点

注意关闭宿主机上 6379 端口 Redis 的服务。

  • 进入主节点容器,启动 Ray Head 节点。node-ip-address请根据实际情况使用上一步,查询到的可用 IP。
# 进入主节点容器
docker exec -it ray-node-0 bash
# 启动 Ray Head 节点
ray start --head \
 --node-ip-address=10.193.195.59 \
 --port=6379\
 --dashboard-host=0.0.0.0 \
 --num-cpus=8\
 --num-gpus=1
  • 启动成功标志:终端输出 Ray runtime started.,并显示从节点连接命令。

4.3 从节点容器加入 Ray 集群

  • 进入从节点容器,加入 Ray 集群,address需要填写主节点给出的连接 IP 10.193.195.59:6379。
# 进入从节点容器
docker exec -it ray-node-1 bash
# 启动 Ray 从节点
ray start --address='10.193.195.59:6379' --node-ip-address='10.193.195.59' --num-cpus=8 --num-gpus=1
  • 成功标志:终端输出Worker node connected to head node。

4.4 验证 Ray 集群状态

  • 在主节点容器内,查看 ray 集群状态。
# 进入主节点容器
docker exec -it ray-node-0 bash
# 查看集群节点
ray status
# 输出:2 个节点(head+worker),每个节点 1 个 GPU,总计 2 个 GPU/32 个 CPU
root@yang-server:~/ray-vllm# ray status
======== Autoscaler status: 2026-01-29 05:14:29.925034 ========
Node status ---------------------------------------------------------------
Active: 1 node_b4f370771c977c241f30a026e0c9ddfb4ee38a38d01eb731f9f72efd
1 node_802f333067990b56926977df9b233c5fa021783653ba92413e0a36ca
Resources ---------------------------------------------------------------
Total Usage: 0.0/16.0 CPU
0.0/2.0 GPU
0B/42.89GiB memory
0B/18.38GiB object_store_memory

4.5 查看 Ray 仪表盘

可视化验证:宿主机浏览器打开http://localhost:8265,进入 Ray 仪表盘,在 Cluster 能看到 2 个节点,GPUs 显示 2,即集群搭建成功。(每个节点虽然显示两个 GPU,但实际只使用一个 GPU)

[图:Ray Dashboard 界面]

4.6 节点内验证 Ray 集群状态

  • 在主节点挂载目录/opt/ray-vllm/data-0/,创建测试脚本。
cd /opt/ray-vllm/data-0/
vim check_resources.py
  • 填入脚本内容。
import ray
# 连接到 Ray 集群
ray.init(address='auto', _node_ip_address='10.193.195.59')
# 获取集群资源
cluster_resources = ray.cluster_resources()
available_resources = ray.available_resources()
print("--- 集群总资源 ---")
for resource, value in cluster_resources.items():
    print(f"{resource}: {value}")
print("\n--- 可用资源 ---")
for resource, value in available_resources.items():
    print(f"{resource}: {value}")
# 获取所有节点
nodes = ray.nodes()
print(f"\n--- 节点数量:{len(nodes)} ---")
for node in nodes:
    print(f"节点 ID: {node['NodeID']}")
    print(f"节点 IP: {node['NodeManagerAddress']}")
    print(f"节点资源:{node['Resources']}")
    print("---")
# 断开连接
ray.shutdown()
  • 执行脚本,可以清楚地看到每个容器内的 GPU 数量为 1。
root@yang-server:~/ray-vllm/data# python3 check_resources.py
2026-01-29 05:35:34,420 INFO worker.py:1821 -- Connecting to existing Ray cluster at address: 10.193.195.59:6379...
2026-01-29 05:35:34,434 INFO worker.py:1998 -- Connected to Ray cluster. View the dashboard at http://10.193.195.59:8265
/usr/local/lib/python3.12/dist-packages/ray/_private/worker.py:2046: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0 warnings.warn(
--- 集群总资源 ---
object_store_memory: 19738098892.0
GPU: 2.0
CPU: 16.0
accelerator_type:G: 2.0
memory: 46055564084.0
node:10.193.195.59: 2.0
node:__internal_head__: 1.0
--- 可用资源 ---
object_store_memory: 19738098892.0
GPU: 2.0
CPU: 16.0
node:10.193.195.59: 2.0
accelerator_type:G: 2.0
memory: 46055564084.0
node:__internal_head__: 1.0
--- 节点数量:2 ---
--- 节点 ID: b4f370771c977c241f30a026e0c9ddfb4ee38a38d01eb731f9f72efd
节点 IP: 10.193.195.59
节点资源:{'memory':22714964788.0, 'object_store_memory':9734984908.0, 'node:__internal_head__':1.0, 'accelerator_type:G':1.0, 'GPU':1.0, 'CPU':8.0, 'node:10.193.195.59':1.0}
--- 节点 ID: 802f333067990b56926977df9b233c5fa021783653ba92413e0a36ca
节点 IP: 10.193.195.59
节点资源:{'memory':23340599296.0, 'object_store_memory':10003113984.0, 'accelerator_type:G':1.0, 'GPU':1.0, 'CPU':8.0, 'node:10.193.195.59':1.0}
---

五 模型下载和运行

5.1 下载 Qwen3-4B 模型

  1. 在宿主机 base 环境下,执行命令下载模型。
cd /opt/ray-vllm/data-0/
pip install modelscope
modelscope download --model Qwen/Qwen3-4B-Instruct-2507 --local_dir ./Qwen3-4B-Instruct-2507
  1. 验证容器内可访问模型
docker exec -it ray-node-0 bash
cd /root/ray-vllm/data
ls -l

5.2 Ray+vLLM 分布式运行 Qwen3-4B 大模型

  • 进入主节点容器,切换到模型目录。
docker exec -it ray-node-0 bash
cd /root/ray-vllm/data
  • 执行 vllm 运行模型。
vllm serve ./Qwen3-4B-Instruct-2507 \
 --tensor-parallel-size 2\
 --gpu-memory-utilization 0.75\
 --max-model-len 4096\
 --host 0.0.0.0 \
 --port 8000

5.3 查看 GPU 运行状态

  • 在宿主机安装nvitop监视 gpu 情况。
pip install nvitop
nvitop -m full

[图:GPU 监控界面]

六 交互式分布式推理(快速测试)

6.1 脚本测试

  • 进入主节点容器,切换到模型目录。
docker exec -it ray-node-0 bash
cd /root/ray-vllm/data
  • 创建并编辑测试脚本。
vim test.py
from openai import OpenAI
client = OpenAI(
 base_url="http://127.0.0.1:8000/v1",# vLLM OpenAI 兼容地址
 api_key="dummy"# 本地 vLLM 一般不校验,随便填)
stream = client.chat.completions.create(
 model="./Qwen3-4B-Instruct-2507",
 messages=[{"role":"system","content":"你是一个有用的助手。"},{"role":"user","content":"请写一首七言绝句,赞美伟大的领袖。要求:磅礴大气,情感深厚。"}],
 temperature=0.7,
 max_tokens=3072,
 stream=True,# 关键:开启流式)
# 逐块打印生成内容
for chunk in stream:
 # 每个 chunk 里可能有 choices[0].delta.content
 if chunk.choices and chunk.choices[0].delta.content is not None:
     print(chunk.choices[0].delta.content, end="", flush=True)
print()# 最后换行
  • 执行脚本获得输出。
python3 test.py
万里江山披赤帜,千秋伟业铸雄魂。风雷激荡开新纪,一炬长明照乾坤。 ——此诗以'万里江山''千秋伟业'展现宏阔气度,以'风雷激荡''一炬长明'抒发深沉敬意,赞颂领袖胸怀天下、扭转乾坤的非凡气魄,情感磅礴,气象雄浑。
  • 主节点终端日志如下:
(APIServer pid=1217) INFO: 127.0.0.1:50880 - "POST /v1/chat/completions HTTP/1.1"200 OK
(APIServer pid=1217) INFO 01-29 14:14:23 [loggers.py:257] Engine 000: Avg prompt throughput: 4.0 tokens/s, Avg generation throughput: 14.7 tokens/s, Running: 0 reqs, Waiting: 0 reqs, GPU KV cache usage: 0.0%, Prefix cache hit rate: 0.0%

6.2 网页对话交互测试

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 对话助手 (vLLM)</title>
<style>
:root{--primary-color: #4f46e5;/* 靛蓝色 */--primary-hover: #4338ca;--bg-color: #f3f4f6;--chat-bg: #ffffff;--text-main: #111827;--text-secondary: #6b7280;--border-color: #e5e7eb;--user-bubble-bg: #4f46e5;--user-bubble-text: #ffffff;--ai-bubble-bg: #f9fafb;--ai-bubble-text: #1f2937;--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);}
*{box-sizing: border-box;margin: 0;padding: 0;}
body{font-family: -apple-system, BlinkMacSystemFont,"Segoe UI", Roboto, Helvetica, Arial, sans-serif;background-color:var(--bg-color);color:var(--text-main);height: 100vh;display: flex;flex-direction: column;overflow: hidden;/* 防止整个页面滚动 */}
/* 顶部导航 */header{background:var(--chat-bg);padding: 1rem 1.5rem;border-bottom: 1px solid var(--border-color);display: flex;justify-content: space-between;align-items: center;flex-shrink: 0;z-index: 10;}.brand{font-size: 1.25rem;font-weight: 700;color:var(--text-main);display: flex;align-items: center;gap: 0.5rem;}.brand svg{color:var(--primary-color);}.settings-toggle{background: transparent;border: 1px solid var(--border-color);padding: 0.5rem;border-radius: 0.5rem;cursor: pointer;color:var(--text-secondary);transition: all 0.2s;}.settings-toggle:hover{background:var(--bg-color);color:var(--text-main);}
/* 设置面板(可折叠) */#settings-panel{background:var(--chat-bg);border-bottom: 1px solid var(--border-color);padding: 1rem 1.5rem;display: none;/* 默认隐藏 */grid-template-columns:repeat(auto-fit,minmax(200px, 1fr));gap: 1rem;animation: slideDown 0.3s ease-out;flex-shrink: 0;}#settings-panel.open{display: grid;}@keyframes slideDown{from{opacity: 0;transform:translateY(-10px);}to{opacity: 1;transform:translateY(0);}}.input-group{display: flex;flex-direction: column;gap: 0.25rem;}.input-group label{font-size: 0.75rem;font-weight: 600;color:var(--text-secondary);text-transform: uppercase;letter-spacing: 0.05em;}.input-group input{padding: 0.5rem;border: 1px solid var(--border-color);border-radius: 0.375rem;font-size: 0.875rem;outline: none;transition: border-color 0.2s;}.input-group input:focus{border-color:var(--primary-color);box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);}
/* 聊天主区域 */#chat-container{flex: 1;overflow-y: auto;padding: 1.5rem;display: flex;flex-direction: column;gap: 1.5rem;scroll-behavior: smooth;}.message-wrapper{display: flex;gap: 1rem;max-width: 800px;margin: 0 auto;width: 100%;animation: fadeIn 0.3s ease-out;}@keyframes fadeIn{from{opacity: 0;transform:translateY(5px);}to{opacity: 1;transform:translateY(0);}}.avatar{width: 36px;height: 36px;border-radius: 6px;flex-shrink: 0;display: flex;align-items: center;justify-content: center;font-size: 1.2rem;}.user-msg .message-wrapper{flex-direction: row-reverse;}.user-msg .avatar{background:var(--user-bubble-bg);color: white;}.ai-msg .avatar{background: #10a37f;/* ChatGPT 绿色风格 */color: white;}.message-content{padding: 0.75rem 1rem;border-radius: 12px;font-size: 1rem;line-height: 1.6;position: relative;word-wrap: break-word;max-width: 85%;white-space: pre-wrap;}.user-msg .message-content{background:var(--user-bubble-bg);color:var(--user-bubble-text);border-top-right-radius: 2px;}.ai-msg .message-content{background:var(--ai-bubble-bg);color:var(--ai-bubble-text);border: 1px solid var(--border-color);border-top-left-radius: 2px;}
/* 打字动画 */.typing-indicator{display: flex;gap: 4px;padding: 4px 0;}.typing-dot{width: 6px;height: 6px;background: #9ca3af;border-radius: 50%;animation: bounce 1.4s infinite ease-in-out both;}.typing-dot:nth-child(1){animation-delay: -0.32s;}.typing-dot:nth-child(2){animation-delay: -0.16s;}@keyframes bounce{0%, 80%, 100%{transform:scale(0);}40%{transform:scale(1);}}
/* 底部输入区 */.input-area{background:var(--chat-bg);padding: 1.5rem;border-top: 1px solid var(--border-color);flex-shrink: 0;}.input-box-wrapper{max-width: 800px;margin: 0 auto;position: relative;background: #fff;border: 1px solid var(--border-color);border-radius: 0.75rem;box-shadow:var(--shadow-sm);transition: box-shadow 0.2s, border-color 0.2s;display: flex;align-items: flex-end;padding: 0.5rem;}.input-box-wrapper:focus-within{border-color:var(--primary-color);box-shadow:var(--shadow-md);}textarea{flex: 1;border: none;resize: none;padding: 0.75rem;font-size: 1rem;font-family: inherit;max-height: 200px;min-height: 24px;outline: none;background: transparent;line-height: 1.5;}.send-btn{background:var(--primary-color);color: white;border: none;border-radius: 0.5rem;width: 36px;height: 36px;display: flex;align-items: center;justify-content: center;cursor: pointer;margin-bottom: 2px;margin-right: 2px;transition: background 0.2s;}.send-btn:hover{background:var(--primary-hover);}.send-btn:disabled{background: #d1d5db;cursor: not-allowed;}
/* 滚动条美化 */#chat-container::-webkit-scrollbar{width: 8px;}#chat-container::-webkit-scrollbar-track{background: transparent;}#chat-container::-webkit-scrollbar-thumb{background-color: #d1d5db;border-radius: 4px;}#chat-container::-webkit-scrollbar-thumb:hover{background-color: #9ca3af;}
/* 响应式调整 */@media(max-width: 640px){header{padding: 0.75rem 1rem;}#chat-container{padding: 1rem;}.message-wrapper{max-width: 100%;}.message-content{max-width: 90%;font-size: 0.95rem;}.input-area{padding: 1rem;}}
</style>
</head>
<body>
<header><div class="brand"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10H12V2z"></path><path d="M12 12 2.1 12a10.1 10.1 0 0 0 10 10V12z"></path><path d="M12 12 21.9 12a10.1 10.1 0 0 0-10-10V12z"></path></svg><span>vLLM Chat</span></div><button id="settings-toggle" class="settings-toggle" title="设置"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></button></header>
<div id="settings-panel"><div class="input-group"><label>API Base URL</label><input id="base-url" type="text" value="http://127.0.0.1:8000/v1"></div><div class="input-group"><label>模型名称</label><input id="model-name" type="text" value="./Qwen3-4B-Instruct-2507"></div><div class="input-group"><label>API Key</label><input id="api-key" type="text" value="dummy"></div></div>
<main id="chat-container"><!-- 欢迎消息 --><div class="message-wrapper ai-msg"><div class="avatar"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg></div><div class="message-content">你好!我是 vLLM 助手。有什么我可以帮你的吗?</div></div></main>
<footer class="input-area"><div class="input-box-wrapper"><textarea id="user-input" rows="1" placeholder="输入消息... (Shift + Enter 换行)"></textarea><button id="send-btn" class="send-btn" disabled><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg></button></div></footer>
<script>
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const settingsToggle = document.getElementById('settings-toggle');
const settingsPanel = document.getElementById('settings-panel');
const baseUrlInput = document.getElementById('base-url');
const modelNameInput = document.getElementById('model-name');
const apiKeyInput = document.getElementById('api-key');
// 切换设置面板
settingsToggle.addEventListener('click',()=>{
 settingsPanel.classList.toggle('open');
});
// 输入框自动增高
userInput.addEventListener('input',function(){
this.style.height ='auto';
this.style.height =(this.scrollHeight)+;
 sendBtn. =..()===;
});

 (){
 wrapper = .();
 wrapper. =;
 avatar = .();
 avatar. =;
(role ===){
 avatar. =;
}{
 avatar. =;
}
 content = .();
 content. =;
 wrapper.(avatar);
 wrapper.(content);
{ wrapper, content };
}

 (){
 content = wrapper.();
 indicator = .();
 indicator. =;
 indicator. =;
 indicator. =;
 content.(indicator);
}

 (){
 indicator = wrapper.();
(indicator) indicator.();
}

 (){
 msgs =[];
 wrappers = chatContainer.();
 wrappers.({

(w.(). ===);
 isUser = w..();
 text = w.().;
(text &&!text.()){
 msgs.({: isUser ?:,: text });
}});
 msgs;
}
  (){
 question = userInput..();
(!question);
 baseUrl = baseUrlInput..(,);
 modelName = modelNameInput.;
 apiKey = apiKeyInput.;

 userInput. =;
 userInput.. =;
 sendBtn. =;
 {: userWrapper }=();
 userWrapper.(). = question;
 chatContainer.(userWrapper);
 {: aiWrapper,: aiContent }=();
 chatContainer.(aiWrapper);
(aiWrapper);
 chatContainer. = chatContainer.;
 messages =[{:,:},...(),{:,: question }];
 url =;
{
 response = (url,{:,:{:,:},:.({: modelName,: messages,:,:,:})});
(!response.){
 errText = response.();
();
}
 reader = response..();
 decoder = ();
 buffer =;
 firstChunk =;
(){
 { done, value }= reader.();
(done);
 buffer += decoder.(value,{:});
 lines = buffer.();
 buffer = lines.()||;
( line  lines){
(!line.()|| line.());
(line.()===);
(line.()){
(firstChunk){
(aiWrapper);
 aiContent. =;
 firstChunk =;
}
 jsonStr = line.();
{
 data =.(jsonStr);
 delta = data. && data.[]&& data.[].;
 content = delta && delta.;
(content){
 aiContent. += content;
 chatContainer. = chatContainer.;
}}(e){
 .(, e);
}}
}}
}(err){
(aiWrapper);
 aiContent. +=;
 aiContent.. =;
}{
 sendBtn. = userInput..()===;
 userInput.();
}}
sendBtn.(, doChat);
userInput.(,{
(e. ===&&!e.){
 e.();
();
}});
</script>
</body>
</html>

[图:网页对话界面]

'px'
disabled
this
value
trim
''
// 创建消息 DOM 结构
function
createMessageElement
role
const
document
createElement
'div'
className
`message-wrapper ${role ==='user'?'user-msg':'ai-msg'}`
const
document
createElement
'div'
className
'avatar'
if
'user'
innerHTML
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`
else
innerHTML
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" rx="2"></rect><circle cx="12" cy="5" r="2"></circle><path d="M12 7v4"></path><line x1="8" y1="16" x2="8" y2="16"></line><line x1="16" y1="16" x2="16" y2="16"></line></svg>`
const
document
createElement
'div'
className
'message-content'
appendChild
appendChild
return
// 添加'正在思考'动画
function
showTypingIndicator
wrapper
const
querySelector
'.message-content'
const
document
createElement
'div'
className
'typing-indicator'
id
'typing-indicator'
innerHTML
` <div></div> <div></div> <div></div> `
appendChild
// 移除'正在思考'动画
function
removeTypingIndicator
wrapper
const
querySelector
'#typing-indicator'
if
remove
// 获取历史消息(排除欢迎语)
function
getMessages
const
const
querySelectorAll
'.message-wrapper'
forEach
w=>
// 如果有 id 且是欢迎语逻辑(这里简单判断,实际可以用 class 过滤)
if
querySelector
'.message-content'
textContent
"你好!我是 vLLM 助手。有什么我可以帮你的吗?"
return
const
classList
contains
'user-msg'
const
querySelector
'.message-content'
textContent
if
includes
'...'
push
role
'user'
'assistant'
content
return
async
function
doChat
const
value
trim
if
return
const
value
replace
/\/+$/
''
const
value
const
value
// UI 状态更新
value
''
style
height
'auto'
// 重置高度
disabled
true
// 1. 添加用户消息
const
wrapper
createMessageElement
'user'
querySelector
'.message-content'
textContent
appendChild
// 2. 添加助手消息占位符(带动画)
const
wrapper
content
createMessageElement
'assistant'
appendChild
showTypingIndicator
scrollTop
scrollHeight
// 构建消息列表
const
role
'system'
content
'你是一个有用的助手。'
getMessages
role
'user'
content
const
`${baseUrl}/chat/completions`
try
const
await
fetch
method
'POST'
headers
'Content-Type'
'application/json'
'Authorization'
`Bearer ${apiKey}`
body
JSON
stringify
model
messages
temperature
0.7
max_tokens
3072
stream
true
if
ok
const
await
text
thrownewError
`请求失败:${response.status}${errText}`
const
body
getReader
const
new
TextDecoder
'utf-8'
let
''
let
true
while
true
const
await
read
if
break
decode
stream
true
const
split
/\r?\n/
pop
''
for
const
of
if
trim
startsWith
':'
continue
if
trim
'data: [DONE]'
continue
if
startsWith
'data: '
if
removeTypingIndicator
textContent
''
false
const
slice
6
try
const
JSON
parse
const
choices
choices
0
choices
0
delta
const
content
if
textContent
scrollTop
scrollHeight
catch
console
warn
'解析行失败'
catch
removeTypingIndicator
textContent
`\n[错误:${err.message}]`
style
color
'#ef4444'
finally
disabled
value
trim
''
focus
addEventListener
'click'
// 支持回车发送(Shift+Enter 换行)
addEventListener
'keydown'
(e)=>
if
key
'Enter'
shiftKey
preventDefault
doChat
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • ELK 7.7.1 多条件查询实战:Java API 构建 BoolQuery
  • 部署开源智能体网关 OpenClaw,将 AI 接入聊天软件
  • Thread Sanitizer:在发版前捕获 C++ 数据竞争
  • WSL2 启动报错 0x8007054f 及网络配置问题解决方案
  • Trae 集成 Figma MCP 实现前端代码自动生成
  • 前端常用动画库:GSAP、Lottie、Swiper 与 AOS 实战指南
  • Spring AI:Java 生态的 AI 应用开发框架与实践
  • 哈希集合巧解最长连续序列
  • MySQL 8.0 Windows 详细安装配置指南
  • MySQL 8.0 Windows 安装配置实战指南
  • 基于 YOLOv 的 Web 目标检测系统:从模型导出到生产部署实战
  • 前端 GraphQL 客户端:高效获取数据的最佳实践
  • Nanbeige 4.1-3B Streamlit WebUI 添加对话评分与反馈功能
  • HarmonyOS 6.0 Camera Kit 微距状态监听能力详解
  • 实战:使用 Jenkins Pipeline 自动化构建与部署 Java 项目
  • Qwen3-VL 模型在 ComfyUI 中的本地部署指南
  • C++ 笔试算法实战:打怪模拟、字符串分类与连通分量
  • 黑客、骇客、红客、蓝客与飞客的定义及区别解析
  • FL-NAS: 基于大语言模型的资源受限设备 NAS 公平性研究
  • 基于 Java + SpringBoot + Vue 的图书大厦管理系统设计与实现

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online