目录

目录

PVE GPU 透传实战:4 卡 RTX 3090 与 vLLM 推理

目录

多 GPU 服务器上做 PCI Passthrough,和单卡消费级桌面是完全不同的工程。IOMMU 分组不可控、无板载显卡的管理面缺失、PCIe 拓扑对 P2P 通信的影响、长时间负载下的热稳定性,这些问题在网上大量的“透传教程”里几乎不会被提到,但也恰恰是生产环境中最容易翻车的地方。

本文以一台 4 卡 RTX 3090 的无头机为主,记录 PVE 下 GPU 透传的配置流程、踩坑细节和实测数据(CUDA 带宽、PyTorch 训练、vLLM 推理)。另有一台 8 卡 V100 的数据中心级平台,配置差异会一并说明;V100 的性能数据尚未采集,文中不编造数字,相关部分只保留方法。

项目 平台 A:4卡 RTX 3090 平台 B:8卡 V100
宿主机 IP 192.168.1.2 192.168.1.3
GPU NVIDIA RTX 3090 x4 NVIDIA V100 (SXM2/PCIe) x8
CPU AMD Ryzen Threadripper 3960X (24C/48T) Dual Intel Xeon Gold 6138 @ 2.00GHz (40C/80T)
内存 128 GB (32GB x4) DDR4 256 GB ECC DDR4
主板/机架 ASRock TRX40 Creator 浪潮 Inspur NF5468M5
板载显卡 无 带有独立 AST2500 BMC
管理方式 纯 IP / SSH IPMI 带外管理

为了让 GPU 推理层、LLM 网关、OpenClaw Agent 以及监控栈能够安全高效地协作,我们首先对物理机和虚拟机进行网段规划。所有节点通过 PVE 的虚拟网桥(如 vmbr0)在二层互通:

节点角色 IP 地址 宿主机位置 核心组件
PVE 宿主机 1 192.168.1.2 物理机 A PVE 系统、PCIe 直通管理
PVE 宿主机 2 192.168.1.3 物理机 B PVE 系统、NVLink / PCIe 直通管理
3090 推理 VM 192.168.1.20 PVE 1 (平台 A) vLLM 实例(挂载 4x 3090)
V100 推理 VM 192.168.1.21 PVE 2 (平台 B) vLLM 实例(挂载 8x V100)
LLM 路由网关 192.168.1.12 PVE 1 或 2 LiteLLM (统一 API 网关)、鉴权与限流
OpenClaw VM 192.168.1.11 PVE 1 或 2 OpenClaw Gateway、Agent、Docker Sandbox
自动化工作流 192.168.1.13 PVE 1 或 2 n8n (通过 Agent 触发)
LLM Tracing 192.168.1.14 PVE 1 或 2 Langfuse(LLM 链路审计与观测)
全局监控节点 192.168.1.30 PVE 1 或 2 Prometheus、Grafana

整个 Homelab 的多模型 AI Agent 架构与网络拓扑如下:

graph TB
    subgraph Management["管理平面 (192.168.1.x)"]
        PROM["全局监控节点<br/>192.168.1.30<br/>Prometheus/Grafana"]
        LF["LLM Tracing 节点<br/>192.168.1.14<br/>Langfuse"]
        PVE1["PVE 宿主机 1<br/>192.168.1.2<br/>(4x RTX 3090)"]
        PVE2["PVE 宿主机 2<br/>192.168.1.3<br/>(8x V100)"]
    end

    subgraph Application["应用平面 (192.168.1.x)"]
        OC["OpenClaw Agent<br/>192.168.1.11"]
        N8N["n8n 工作流<br/>192.168.1.13"]
    end

    subgraph Gateway["网关平面"]
        LITE["LLM 路由网关<br/>192.168.1.12<br/>LiteLLM Proxy"]
    end

    subgraph Compute["GPU 推理层"]
        VM3090["3090 推理 VM<br/>192.168.1.20<br/>vLLM"]
        VMV100["V100 推理 VM<br/>192.168.1.21<br/>vLLM"]
    end

    %% 业务流
    OC -->|"HTTPS+Key"| LITE
    N8N -->|"HTTPS+Key"| LITE
    LITE -->|"localhost(回环) / 直连"| VM3090
    LITE -->|"直连"| VMV100
    
    %% 审计和监控流
    LITE -.->|"异步 tracing"| LF
    PROM -.->|"拉取指标"| VM3090
    PROM -.->|"拉取指标"| VMV100
    PROM -.->|"拉取指标"| PVE1
    PROM -.->|"拉取指标"| PVE2

    classDef vm fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;
    classDef host fill:#f5f5f5,stroke:#424242,stroke-width:2px;
    classDef gw fill:#fff3e0,stroke:#e65100,stroke-width:2px;
    
    class OC,N8N,VM3090,VMV100,LF vm;
    class LITE,PROM gw;
    class PVE1,PVE2 host;

架构注记:

  1. LLM 路由网关 (1.12) 是所有流量的汇聚点。无论是 OpenClaw (1.11) 还是 n8n (1.13),都禁止直连底层的推理 VM (1.20 和 1.21)。
  2. 只有 LLM 路由网关拥有底层推理 VM 的访问权限(端口 8001-8003 等),从而将安全边界收拢到网关一层。

两个平台的差异不只是卡的型号,而是体现在整个 I/O 架构上:

平台 A(4 卡 3090,ASRock TRX40) 的核心挑战是无板载显卡。这不是一个小问题——它直接决定了管理面的可用性。如果 4 张 GPU 全部 VFIO 绑定,宿主机将没有任何帧缓冲设备,GRUB 引导画面都不会有。

本文针对平台 A 采用的方案是:纯 SSH + PVE Web UI 管理。PVE 管理面本身不依赖 GPU。只要网络通,Web UI 照常可用。一旦配置出错导致断网,唯一的退路是接上硬盘离线修改网络配置,或者在主板上临时插一张亮机卡。

平台 B(8 卡 V100,浪潮 NF5468M5) 是标准机架式 AI 服务器,带有独立的 BMC/IPMI 管理口,所以无图形界面管理不是问题。但它的复杂度在于 NUMA 节点绑定和 PCIe 拓扑。双路 Intel Xeon Gold 设计意味着 8 张卡实际上是挂载在不同的 CPU Socket 上的。如果虚拟机跨 NUMA 节点调度,或者直通了不属于同一个 CPU 的网卡和 GPU,会导致极大的 QPI/UPI 互连延迟惩罚。

BIOS 配置是 GPU 透传的地基。漏一项可能就是启动黑屏或者 BAR 分配失败。以下是必须确认的项:

BIOS 选项 要求 备注
VT-d / AMD-Vi 启用 IOMMU 的基础,不开则直接不可用
SR-IOV 建议启用 为后续 vGPU 方案预留
Above 4G Decoding 必须启用 多卡必备。3090 单卡 BAR1 就 256MB,4 卡的 BAR 空间远超 4GB 寻址上限
Resizable BAR (ReBAR) 视情况关闭 部分固件/QEMU 版本下 ReBAR 会导致 VM 内 GPU 初始化失败(Code 43)
CSM 关闭 OVMF 需要纯 UEFI 环境,CSM 兼容模式会引入不可预期的问题

经验之谈:Above 4G Decoding 是多卡透传中最常被忽略的选项。症状是虚拟机内 nvidia-smi 输出 Unable to determine the device handle for GPU,dmesg 里有 BAR 1: can't assign 错误。

编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX_DEFAULT 中添加:

Intel 平台:

GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt pcie_acs_override=downstream,multifunction video=efifb:off"

AMD 平台:

# AMD 平台用 amd_iommu=on,Intel 平台换成 intel_iommu=on
GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt pcie_acs_override=downstream,multifunction video=efifb:off"

逐项说明:

参数 作用 为什么需要
intel_iommu=on / amd_iommu=on 启用 IOMMU 透传的前提条件
iommu=pt Pass-Through 模式 非透传设备走 DMA 直通路径,避免不必要的 IOMMU 翻译开销
pcie_acs_override=downstream,multifunction 强制拆分 IOMMU 分组 多卡服务器上极大概率需要——PCIe Switch 下挂的多张 GPU 往往被分到同一个 IOMMU Group
video=efifb:off 关闭 EFI 帧缓冲 阻止宿主机内核绑定 GPU 的帧缓冲设备,确保 VFIO 能干净地接管
update-grub && reboot

重启后确认:

dmesg | grep -e DMAR -e IOMMU
# 应出现 "DMAR: IOMMU enabled" 或 "AMD-Vi: Initialized"

编辑 /etc/modules:

vfio
vfio_iommu_type1
vfio_pci

Linux 6.2 之后 vfio_virqfd 已经被合并进 VFIO,不用再单独加载。旧教程里如果还有这行,删掉即可。

IOMMU 分组直接决定了透传的可行性和颗粒度。组内设备要么全部透传,要么全留给宿主机。

#!/bin/bash
shopt -s nullglob
for g in $(find /sys/kernel/iommu_groups/* -maxdepth 0 -type d | sort -V); do
    echo "IOMMU Group ${g##*/}:"
    for d in $g/devices/*; do
        echo -e "\t$(lspci -nns ${d##*/})"
    done;
done;

实际分组输出因主板和 BIOS 而异,这里不贴固定结果。运行上面的脚本后,重点看两件事:

  • 每张 3090 是否和它的 HDMI Audio Controller 在同一个 Group。透传时要把显示和音频两个 function 一起给虚拟机。
  • 多张卡是否被合并到同一组。这在多路 PCIe Switch 拓扑中很常见,必要时只能靠 pcie_acs_override 拆组。拆组会绕过 ACS(Access Control Services)的隔离作用,安全敏感场景要评估风险。

V100 SXM2 的 PCIe 拓扑比消费级平台复杂得多,NVLink Bridge 设备和 PLX Switch 都可能出现在 IOMMU 分组中。需要重点确认:分组是否允许独立透传单张 GPU,以及 NVLink 互联设备有没有被一起分组。

lspci -nn | grep -i nvidia

输出示例(实际 bus 地址按你的机器替换):

41:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA102 [GeForce RTX 3090] [10de:2204] (rev a1)
41:00.1 Audio device [0403]: NVIDIA Corporation GA102 High Definition Audio Controller [10de:1aef] (rev a1)

绑定 VFIO:

echo "options vfio-pci ids=10de:2204,10de:1aef disable_vga=1" > /etc/modprobe.d/vfio.conf

disable_vga=1 是关键。没有这个选项,VFIO 在绑定 VGA class 设备时会尝试做 VGA arbitration,在无板载显卡的平台上直接导致绑定失败或内核 hang。

cat >> /etc/modprobe.d/blacklist.conf << 'EOF'
blacklist nouveau
blacklist nvidia
blacklist nvidiafb
blacklist nvidia_drm
EOF

update-initramfs -u -k all && reboot

重启后确认绑定状态:

lspci -nnk -s 41:00
# Kernel driver in use: vfio-pci

如果看到 Kernel driver in use: nouveau 或 nvidia,说明 blacklist 没生效——检查 initramfs 是否正确更新,或检查是否有其他 modprobe 配置文件在加载 NVIDIA 模块。

配置项 推荐值 原因
机器类型 q35 i440fx 的 PCIe 模拟有限,q35 提供完整的 PCIe 拓扑支持
BIOS OVMF (UEFI) NVIDIA GPU 的 VBIOS 需要 UEFI 环境初始化
CPU 类型 host 暴露真实 CPU 特性(如 AVX-512),避免透传后 GPU 驱动因 CPU feature 缺失报错
内存 固定分配,禁用 ballooning 内存气球和 VFIO 设备的 DMA 映射冲突,开 ballooning 大概率导致 VM crash

直接编辑 /etc/pve/qemu-server/<VMID>.conf:

# 单卡透传
hostpci0: 0000:41:00,pcie=1,x-vga=1

# 4 卡 RTX 3090 全部透传至同一 VM
hostpci0: 0000:41:00,pcie=1,x-vga=1
hostpci1: 0000:61:00,pcie=1
hostpci2: 0000:81:00,pcie=1
hostpci3: 0000:a1:00,pcie=1

x-vga=1 只在主 GPU 上设置。它通知 QEMU 将该设备的 VGA 仲裁权交给虚拟机。多卡场景下,只有 hostpci0 需要这个标志。

Web UI 中添加 PCI 设备时需要注意:勾选 All Functions(同时透传GPU和关联的 Audio Controller)、ROM-Bar、PCI-Express。Primary GPU 仅对需要 VGA 输出的主卡勾选。

为了保证推理节点绝对干净且没有冗余服务的开销,我们在 PVE VM 中放弃常规 ISO 安装,转而使用 debootstrap 手工构建极简版 Debian 13 工作环境。你可以使用任何 Live CD(比如 SystemRescue)引导 VM。

# 假设目标磁盘为 /dev/sda,使用 fdisk 分区:
# sda1: EFI (FAT32)
# sda2: Boot (Ext4)
# sda3: Root (Ext4)

apt install -y dosfstools cryptsetup efibootmgr debootstrap

mkfs.fat -F32 /dev/sda1
mkfs.ext4 /dev/sda2
mkfs.ext4 /dev/sda3

# 挂载目录结构
mkdir -pv /mnt/debinst
mount /dev/sda3 /mnt/debinst/
mkdir -pv /mnt/debinst/boot
mount /dev/sda2 /mnt/debinst/boot/
mkdir -pv /mnt/debinst/boot/efi
mount /dev/sda1 /mnt/debinst/boot/efi/

加速拉取 Debian 13 (Trixie) 的 amd64 基础包:

debootstrap --arch amd64 trixie /mnt/debinst https://mirrors.tuna.tsinghua.edu.cn/debian/

# 绑定必要的核心文件系统
mount -v -t proc none /mnt/debinst/proc
mount -v --rbind /sys /mnt/debinst/sys
mount -v --rbind /dev /mnt/debinst/dev
mount -v --make-rslave /mnt/debinst/sys
mount -v --make-rslave /mnt/debinst/dev

# 切换到真实的 VM 环境
LANG=C.UTF-8 chroot /mnt/debinst /bin/bash

安装基础工具、配置挂载表及网络拓扑(以 V100 节点 192.168.1.21 为例):

apt update
apt install -y apt-transport-https ca-certificates aptitude vim dosfstools efibootmgr lvm2

# 填入刚装好的系统分区 UUID(可通过 blkid 获取)
cat <<EOF > /etc/fstab
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /          ext4 defaults,relatime 0 1
UUID=xxxx-xxxx                            /boot/efi  vfat defaults 0 1
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /boot      ext4 defaults,relatime 0 1
EOF

# 主机名与 DNS
echo v100-cluster > /etc/hostname
echo nameserver 114.114.114.114 > /etc/resolv.conf    

# 静态网络配置
cat <<EOF > /etc/network/interfaces
auto ens18
iface ens18 inet static
    address 192.168.1.21
    network 192.168.1.0
    netmask 255.255.255.0
    broadcast 192.168.1.255
    gateway 192.168.1.1
EOF

更新清华 Trixie Apts 源并配置本地化:

cat <<EOF > /etc/apt/sources.list
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ trixie main contrib non-free non-free-firmware
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ trixie-updates main contrib non-free non-free-firmware
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ trixie-backports main contrib non-free non-free-firmware
deb https://mirrors.tuna.tsinghua.edu.cn/debian-security trixie-security main contrib non-free non-free-firmware
EOF

aptitude update
aptitude install locales tzdata
dpkg-reconfigure locales
dpkg-reconfigure tzdata
apt-get install linux-image-amd64

# 必须安装 EFI 引导程序
aptitude install grub-efi
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Proxmox
grub-mkconfig -o /boot/grub/grub.cfg

# 开启 root用户的 SSH 登录,对于无头服务器环境非常重要
apt-get install ssh -y
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
passwd

exit
umount -R /mnt/debinst
reboot

重建并重启进入刚搭建好的 Trixie 系统,首要任务是编译加载 NVIDIA 闭源驱动,并跑通 PyTorch 测试,确认底层透传完全可用。

通过 DKMS 手动编译驱动模块,在系统内核更新时具备更好的兼容性和可控性:

apt update
apt-get install -y pciutils linux-headers-$(uname -r) build-essential dkms

# 安装闭源驱动
apt install -y nvidia-driver
dpkg-reconfigure nvidia-kernel-dkms
modprobe nvidia

# 验证透传状态
nvidia-smi
apt install -y python3-venv python3-pip

python3 -m venv gpu_test_env
source gpu_test_env/bin/activate

# 镜像加速
python -m pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple --upgrade pip
pip config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple

# 只安装核心计算库
pip install torch torchvision torchaudio 

# 测试 GPU 是否被 PyTorch 识别
python -c "import torch; print(f'CUDA 核心可用: {torch.cuda.is_available()}'); print(f'GPU 数量: {torch.cuda.device_count()}')"

测试不仅要认出卡,还要认出它们是怎么连通的:

平台 A:4x RTX 3090

nvidia-smi topo -m
	GPU0	GPU1	GPU2	GPU3	CPU Affinity	NUMA Affinity	GPU NUMA ID
GPU0	 X 	PHB	PHB	PHB	0-31	0		N/A
GPU1	PHB	 X 	PHB	PHB	0-31	0		N/A
GPU2	PHB	PHB	 X 	PHB	0-31	0		N/A
GPU3	PHB	PHB	PHB	 X 	0-31	0		N/A
...

解析:由于主板和 CPU PCIe 控制器的调度,RTX 3090 的互联关系是 PHB(通过 CPU 穿越 PCIe 主桥),没有 NVLink 支持。

平台 B:8x V100-SXM2

nvidia-smi topo -m
	GPU0	GPU1	GPU2	GPU3	GPU4	GPU5	GPU6	GPU7	CPU Affinity	NUMA Affinity	GPU NUMA ID
GPU0	 X 	NV2	NV2	NV1	PHB	PHB	PHB	NV1	0-39	0		N/A
GPU1	NV2	 X 	NV1	NV2	PHB	PHB	NV1	PHB	0-39	0		N/A
...

nvidia-smi nvlink -s
GPU 0: Tesla V100-SXM2-32GB (UUID: GPU-4a303885-0edf-7219-d89d-f863a9e11366)
	 Link 0: 25.781 GB/s
	 Link 1: 25.781 GB/s
...

解析:V100 集群中的 NV1 和 NV2 标志以及 NVLink 25.781 GB/s 的链路状态说明 PVE 对 SXM2 架构底板的 PCIe 拓扑和 NVSwitch 直通支持极佳。nvidia-smi topo -m 的输出决定了多卡训练的通信效率。在透传场景下,如果这里降级成了 SYS 或 PIX,会对分布式训练的 AllReduce 性能产生毁灭性打击。

工具 用途 版本
nvbandwidth PCIe 带宽、P2P 通信 v0.8 (Release)
gpu-burn 计算正确性 + 热稳定性 1.1.0 (Commit: 8a24c…)
PyTorch DDP 训练吞吐量 2.4.1+cu121
vLLM 高吞吐 LLM 推理 最新稳定版(按模型支持选版本)
Prometheus + Grafana + dcgm-exporter GPU 全链路可观测性 v2.54 / v11.2 / 3.3.8 (debian12)

由于 P2P 通信和底层硬件带宽是评估透传损耗的核心,这里我们将使用 NVIDIA 官方最新的现代测量工具:nvbandwidth(已全面替代旧版的 cuda-samples 测速库)。

apt install -y git cmake libboost-program-options-dev nvidia-cuda-toolkit build-essential

# 编译最新的 NVIDIA NVBandwidth 工具
git clone https://github.com/NVIDIA/nvbandwidth.git
cd nvbandwidth
cmake . && make

# Host-to-Device / Device-to-Host 带宽
./nvbandwidth -t host_to_device_memcpy_ce
./nvbandwidth -t device_to_host_memcpy_ce

# GPU 间 P2P 带宽 (Device-to-Device)
./nvbandwidth -t device_to_device_memcpy_ce

nvbandwidth 默认已包含最优大小的拷贝策略,从物理层面确保测量结果准确反映透传后 PCIe 通道本身的能力。

H2D / D2H 带宽:

注:平台 A 由于主板通道划分原因,4 张 RTX 3090 的 PCIe 通道分布不均(GPU 0/1 运行在 PCIe 4.0 x16,GPU 2/3 运行在 x8)。

测试项 RTX 3090 (透传)
H2D (GB/s) ~25.4 (x16) / 13.4 (x8)
D2H (GB/s) ~25.2 (x16) / 13.2 (x8)

P2P 多卡通信:

P2P 用 ./nvbandwidth -t device_to_device_memcpy_ce 测。本文没有保留 4 卡 3090 的 P2P 数字,这里不列具体值;判断方法是看带宽有没有断崖式下降。QEMU 暴露给 VM 的 PCIe 拓扑是虚拟的,GPU 之间的 P2P 路径可能被截断,退化成 Host 中转。如果 P2P 带宽明显受限,大概率是 IOMMU 的 ACS 策略阻断了 peer-to-peer 的 DMA 路径。

在运行高性能计算前,必须确认透传卡的状态。

nvidia-smi -q | grep -E "Product Name|GPU UUID|BAR1|Memory"

平台 A:4x RTX 3090

    Product Name                          : NVIDIA GeForce RTX 3090
    GPU UUID                              : GPU-1dc002ea-dc6e-5806-d798-43de15b92028
    FB Memory Usage
    BAR1 Memory Usage
    Conf Compute Protected Memory Usage
        Memory                            : 0 %
        Memory Current Temp               : N/A
        Memory Max Operating Temp         : N/A
    GPU Memory Power Readings
        Memory                            : 405 MHz
        Memory                            : N/A
        Memory                            : N/A
        Memory                            : N/A
        Memory                            : 9751 MHz
    Product Name                          : NVIDIA GeForce RTX 3090
    GPU UUID                              : GPU-5b7a34da-4cbe-86e8-9607-e6d2b336b944
    FB Memory Usage
    BAR1 Memory Usage
    ... (其余 3 张卡同上)

平台 B:8x V100-SXM2-32GB

    Product Name                          : Tesla V100-SXM2-32GB
    GPU UUID                              : GPU-4a303885-0edf-7219-d89d-f863a9e11366
    FB Memory Usage
    BAR1 Memory Usage
    Conf Compute Protected Memory Usage
        Memory                            : 0 %
                Device Memory             : 0
                Texture Memory            : N/A
                Device Memory             : 0
                Texture Memory            : N/A
                ... 
        Memory Current Temp               : 37 C
        Memory Max Operating Temp         : 85 C
    GPU Memory Power Readings
        Memory                            : 877 MHz
        ...

差异分析:透传环境下,虽然两边的卡都正常识别了,但你可以明显察觉到消费级卡和数据中心卡的监控能力差异。RTX 3090 的 VBIOS 未针对企业级环境暴露 Memory Current Temp 等多项核心传感数据(显示为 N/A),而企业级的 V100 即便在虚拟化环境下,其 ECC 显存、纹理缓存状态以及确切的工作温度限制全部完好挂载传递。这在长时间的高压烤机中是极度关键的监控指标。

在执行训练基准测试前,必须在虚拟环境中准备一个极简版的 PyTorch DDP 吞吐量压测脚本(将张量加载进 GPU 来摸平最大算力):

# train_benchmark.py
import torch, time, argparse, os
import torch.nn as nn
import torch.optim as optim
import torchvision.models as models

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--model', default='resnet50')
    parser.add_argument('--batch-size', type=int, default=128)
    parser.add_argument('--epochs', type=int, default=5)
    parser.add_argument('--num-gpus', type=int, default=1)
    parser.add_argument('--local_rank', type=int, default=-1)
    args = parser.parse_args()

    local_rank = int(os.environ.get("LOCAL_RANK", args.local_rank))
    is_ddp = local_rank != -1

    if is_ddp:
        torch.distributed.init_process_group(backend='nccl')
        torch.cuda.set_device(local_rank)
        device = torch.device(f"cuda:{local_rank}")
    else:
        device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    model = models.resnet50().to(device)
    if is_ddp:
        model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank])

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=0.01)
    inputs = torch.randn(args.batch_size, 3, 224, 224).to(device)
    targets = torch.randint(0, 1000, (args.batch_size,)).to(device)

    for epoch in range(args.epochs):
        torch.cuda.synchronize()
        start = time.time()
        for _ in range(50):
            optimizer.zero_grad()
            loss = criterion(model(inputs), targets)
            loss.backward()
            optimizer.step()
        torch.cuda.synchronize()
        
        if local_rank in [-1, 0]:
            throughput = (50 * args.batch_size * (args.num_gpus if is_ddp else 1)) / (time.time() - start)
            print(f"Epoch {epoch} Throughput: {throughput:.2f} images/sec")

if __name__ == '__main__':
    main()
python train_benchmark.py --model resnet50 --batch-size 128 --epochs 5 --num-gpus 1

参数说明:

参数 说明
--model resnet50 使用 torchvision.models.resnet50 作为基准模型,ResNet-50 是业界最通用的训练吞吐量评测标准
--batch-size 128 每次迭代喝入 128 张 224×224 图片,对于 24 GB 显存的 RTX 3090 可以安全填满;如果显存报 OOM,可降为 64
--epochs 5 跑 5 轮,每轮 50 次前向 + 反向传播,前 1-2 轮为预热期,后 3 轮的均值为有效吞吐量
--num-gpus 1 单卡模式,脑本不启用 DDP,纯粹测量单卡算力上限

测试期间同时开启另一个终端运行 watch -n 1 nvidia-smi 来实时监控 GPU 利用率和显存占用,将峰值记录到下方表格:

指标 RTX 3090 (透传)
吞吐量 (images/sec) ~512
GPU 利用率 (%) 100%
显存占用 (MB) 12739MiB / 24576MiB

上面的标准基准测试中,RTX 3090 的显存利用率仅约 52%(12.7 GB / 24 GB)。虽然 GPU 计算单元已经满载,但显存映射路径并没有在压力下被充分验证。在透传环境中,VFIO 的 DMA 映射和 QEMU 的 BAR 地址空间分配都可能在高显存占用时出现问题——这类 Bug 只有在接近显存上限时才会暴露。

逐步加大 batch-size,直到触发 OOM(Out of Memory),记录最后一次稳定运行的配置:

# 逐级递增 batch-size,找到显存上限
python train_benchmark.py --model resnet50 --batch-size 256 --epochs 3 --num-gpus 1
python train_benchmark.py --model resnet50 --batch-size 384 --epochs 3 --num-gpus 1
python train_benchmark.py --model resnet50 --batch-size 512 --epochs 3 --num-gpus 1
# 如果 512 仍然不 OOM,继续尝试 640、768...
指标 RTX 3090 (透传)
最大稳定 batch-size 256 (384 即 OOM)
对应显存占用 23145 MiB / 24576 MiB (94.2%)
对应吞吐量 (images/sec) ~519

这个最大稳定 batch-size 的实际意义:它直接反映了透传环境下 GPU 可用显存的真实上限,而非 nvidia-smi 报告的理论值。后续规划 vLLM 的 --gpu-memory-utilization 参数和 KV Cache 大小时,应以此为准而非卡面标称值。

多卡测试使用 PyTorch 官方的 torchrun 启动器,它会自动为每个进程分配 LOCAL_RANK 环境变量并初始化 NCCL 通信后端。

平台 A:4x RTX 3090

torchrun --nproc_per_node=4 train_benchmark.py --model resnet50 --batch-size 128 --epochs 5

平台 B:8x V100

torchrun --nproc_per_node=8 train_benchmark.py --model resnet50 --batch-size 64 --epochs 5

torchrun 参数说明:

参数 说明
--nproc_per_node=4 每台机器启动的进程数,必须等于可见 GPU 数量(RTX 3090 平台填 4,V100 平台填 8)
--batch-size 128 每张卡的 local batch size,4 卡场景下全局等效 batch = 128 × 4 = 512
--epochs 5 多卡场景下同样 5 轮,观察 AllReduce 同步开销是否拖慢总吞吐量

V100 平台 batch-size 设为 64 而非 128,是因为 V100-32GB 的 HBM2 带宽虽高但计算单元较少,满载 batch-size 128 时计算单元在等待显存读取的时间反而会拉低单卡利用率。

指标 4x RTX 3090 (透传)
单卡吞吐量 (images/sec) ~455
总吞吐量 (images/sec) ~1820
线性扩展比 0.89

线性扩展比 = 多卡总吞吐量 ÷ (单卡吞吐量 × 卡数)。理想值是 1.0,AllReduce 通信开销会让它低于 1。3090 平台没有 NVLink,梯度同步走 PCIe,0.89 已经算不错;V100 的 NVLink 平台如果后续补齐数据,可以再对比。

DDP 训练中,多卡吞吐量的瓶颈主要源于缺失 NVLink,导致 AllReduce 梯度同步只能依赖 PCIe,从而带来较大的通信开销。在大模型或多卡并行训练时,这种通信限制往往会成为影响性能的主导因素。

LLM 推理是 GPU 透传下最典型的工作负载之一。不同框架对底层硬件的使用方式差异很大,因此需要多框架交叉验证,才能准确评估透传层的实际影响。

模型 参数量 精度 显存需求 测试框架
Qwen3.5-9B 9B FP16 ~18 GB vLLM
Qwen3.5-35B-A3B 35B MoE FP16 ~15 GB (激活参数极低) vLLM
Qwen3-Coder-Next ~32B FP16 ~64 GB vLLM
zai-org/GLM-5 ~9B FP16 ~18 GB vLLM
MiniMaxAI/MiniMax-M2.5 MoE FP16 - vLLM (多卡)
Qwen3.5-122B-A10B 122B MoE FP8 / INT4 ~60 GB (量化) vLLM (多卡 TP 必须)

模型选择需要匹配显存容量。RTX 3090 的 24 GB GDDR6X 和 V100 的 16/32 GB HBM2 在带宽特性和容量上有本质差异——这不仅影响能跑多大的模型,也直接影响 KV Cache 容量和最大并发。

vLLM 通过 Continuous Batching + PagedAttention 实现高吞吐推理,是目前生产部署的首选方案。

# 测试节点临时环境使用 pip 部署即可
# 1. 切换 PyPI 清华源加速安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install vllm

# 2. 注入 HF 国内镜像站环境变量,防止起服动态拉取模型时卡死超时
export HF_ENDPOINT="https://hf-mirror.com"


# 9B 2卡
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen3.5-9B \
    --dtype float16 \
    --tensor-parallel-size 2 \
    --gpu-memory-utilization 0.9 \
    --max-model-len 4096 \
    --port 8000


# 9B 4卡
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen3.5-9B \
    --dtype float16 \
    --tensor-parallel-size 4 \
    --gpu-memory-utilization 0.9 \
    --max-model-len 4096 \
    --port 8000

# 多卡 TP=4,跑 122B MoE 模型
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen3.5-122B-A10B \
    --dtype float16 \
    --tensor-parallel-size 4 \
    --gpu-memory-utilization 0.9 \
    --max-model-len 4096 \
    --port 8000

基准测试直接用 vLLM 内置的 benchmark 工具,它会模拟真实的并发请求模式:

export HF_ENDPOINT=https://hf-mirror.com

vllm bench serve \
    --backend openai \
    --base-url http://localhost:8000 \
    --model Qwen/Qwen3.5-9B \
    --num-prompts 1000 \
    --request-rate inf \
    --temperature 0

单卡(9B 模型):

单卡 9B 的 vLLM 数据当时没有保留完整日志,这里不列数字。复现时用上面的 vllm bench serve,把 --num-prompts 和 --request-rate 固定下来再对比。

多卡 TP=4(9B 模型):

这是将 9B 模型切分到 4 张卡上(Tensor Parallel Size = 4)执行推理的结果。由于模型本身较小,将其强行切分到 4 张卡往往会导致显存带宽的收益低于多卡间频繁的通信(AllReduce)开销,这是非常有价值的架构折损测试。

指标 4x RTX 3090 (透传)
成功并发请求数 1000
Total Token Throughput (tokens/s) 2404.30
Generation Throughput (tokens/s) 267.14
Request Throughput (req/s) 2.09
Median TTFT (ms) 258617.31
Median TPOT (ms) 754.74

指标解读:

  • Total Token Throughput (2404 tok/s):系统整体的吞吐表现。包含了 Prompt 解析和 Token 生成。
  • Generation Throughput (267 tok/s):实际生成新字符的速度。因为 TP=4 引入了大量的 PCIe 通信,对于仅 9B 的模型来说,通信延迟(ITL ~747ms)极其严重,导致实际生成速度并不高。
  • Median TTFT (258s):首字延迟。在 --request-rate inf(瞬间并发 1000 个请求)的极端压力下,排队最久的请求花了 4 分多钟才拿到首字,体现了极端负载下的 PagedAttention 调度压力。

多卡 TP(122B 模型):

122B 的 MoE 模型没有跑完完整的 benchmark,本文不列数字。配置上 4 张 3090 需要做 INT4/FP8 量化并按 TP=4 切分;显存和并发上限要在自己的环境里实测。

透传环境的稳定性远比性能更值得关注。一张 GPU 在裸机上跑 24 小时没问题,不代表透传后同样稳定——VFIO 的 DMA 映射、IOMMU 的翻译表、QEMU 的中断路由都是潜在的故障点。用 gpu-burn 做长时间烤机,配合 Prometheus + Grafana + DCGM 做全链路持续监控,才能给出可信的稳定性结论。

git clone https://github.com/wilicc/gpu-burn.git
cd gpu-burn
make CUDAPATH=/usr/local/cuda

./gpu_burn 10  # 先跑 10 秒验证

gpu-burn 的原理是在所有可见 GPU 上并行执行大矩阵乘法,并定期校验计算结果的正确性。输出中出现 FAULTY 表示计算结果不一致——可能原因包括显存物理缺陷、过热导致的位翻转、PCIe 链路不稳定等。这在二手矿卡和长期高负载运行的 V100 上并不罕见。

单纯跑 gpu-burn 只能告诉你"通过"或"失败",但不能回答"为什么"。DCGM Exporter 采集的细粒度时序数据(温度曲线、功耗曲线、时钟频率变化、XID 错误、PCIe Replay 计数等)才能帮你定位根因。

graph TB
    subgraph Visualization["可视化层"]
        GRAFANA["Grafana (:3000)<br/>GPU Monitoring Dashboard"]
    end

    subgraph Collection["数据采集与存储"]
        PROM["Prometheus (:9090)<br/>时序数据采集与存储 (5s / 15s 间隔)"]
    end

    subgraph Exporters["指标导出层"]
        DCGM["dcgm-exporter (:9400)<br/>GPU 指标"]
        NODE["node_exporter (:9100)<br/>系统指标"]
    end

    GRAFANA -->|"PromQL 查询"| PROM
    PROM -->|"scrape"| DCGM
    PROM -->|"scrape"| NODE

DCGM(Data Center GPU Manager)是 NVIDIA 的数据中心级 GPU 管理框架,比 nvidia-smi 暴露的指标更丰富,采集开销更低。

Docker 部署(推荐):

docker run -d --gpus all --restart always \
    --name dcgm-exporter \
    -p 9400:9400 \
    nvcr.io/nvidia/k8s/dcgm-exporter:3.3.8-3.6.0-debian12

curl -s http://localhost:9400/metrics | head -20

裸装(不依赖 Docker):

# 方案 A:DCGM 全量安装
apt install -y datacenter-gpu-manager
nv-hostengine
dcgmi discovery

# 方案 B:轻量级 nvidia_gpu_exporter(仅依赖 nvidia-smi)
wget https://github.com/utkuozdemir/nvidia_gpu_exporter/releases/download/v1.2.1/nvidia-gpu-exporter_1.2.1_linux_amd64.tar.gz
tar -xzf nvidia-gpu-exporter_1.2.1_linux_amd64.tar.gz
./nvidia_gpu_exporter --web.listen-address=":9835" &

DCGM 关键指标:

指标 说明 单位 烤机中的意义
DCGM_FI_DEV_GPU_TEMP GPU 核心温度 °C 判断散热是否充足
DCGM_FI_DEV_GPU_UTIL GPU 利用率 % 应持续 ~100%
DCGM_FI_DEV_POWER_USAGE 实时功耗 W 观察是否触达 TDP 上限
DCGM_FI_DEV_SM_CLOCK SM 时钟 MHz 降频的直接体现
DCGM_FI_DEV_POWER_VIOLATION 功耗限制触发时间 us 非零说明功耗墙在限制性能
DCGM_FI_DEV_THERMAL_VIOLATION 温度限制触发时间 us 非零说明温度墙在限制性能
DCGM_FI_DEV_XID_ERRORS XID 错误计数 次 非零即故障
DCGM_FI_DEV_PCIE_REPLAY_COUNTER PCIe Replay 计数 次 反映 PCIe 链路信号质量
# Node Exporter
docker run -d --restart always \
    --name node-exporter \
    --net="host" --pid="host" \
    -v "/:/host:ro,rslave" \
    quay.io/prometheus/node-exporter:latest \
    --path.rootfs=/host

Prometheus 配置 /opt/monitoring/prometheus/prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'dcgm-exporter'
    scrape_interval: 5s      # GPU 指标用更短的采集间隔
    static_configs:
      - targets: ['localhost:9400']

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['localhost:9100']

  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']
# Prometheus
docker run -d --restart always \
    --name prometheus \
    --net="host" \
    -v /opt/monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
    -v /opt/monitoring/prometheus/data:/prometheus \
    prom/prometheus:latest \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.path=/prometheus \
    --storage.tsdb.retention.time=30d \
    --web.enable-lifecycle

# Grafana
mkdir -p /opt/monitoring/grafana
docker run -d --restart always \
    --name grafana \
    --net="host" \
    -v /opt/monitoring/grafana:/var/lib/grafana \
    -e "GF_SECURITY_ADMIN_PASSWORD=admin" \
    grafana/grafana:latest

Grafana 中导入 DCGM 官方 Dashboard(ID: 12239),然后根据实际需要定制面板。烤机测试中重点关注的 PromQL 查询:

# 温度趋势
DCGM_FI_DEV_GPU_TEMP{job="dcgm-exporter"}

# SM 降频观察
DCGM_FI_DEV_SM_CLOCK{job="dcgm-exporter"}

# 功耗墙触发频率
rate(DCGM_FI_DEV_POWER_VIOLATION{job="dcgm-exporter"}[1m])

# XID 错误(任何非零都是红灯)
DCGM_FI_DEV_XID_ERRORS{job="dcgm-exporter"}

# PCIe Replay 错误趋势
DCGM_FI_DEV_PCIE_REPLAY_COUNTER{job="dcgm-exporter"}

告警规则:GPU 温度 > 85°C、XID 错误 > 0、连续降频 > 5 分钟——这三条覆盖了绝大多数会导致烤机失败的场景。

如果在多台机器上需要反复部署,用 Compose 更高效:

# /opt/monitoring/docker-compose.yml
version: '3.8'

services:
  dcgm-exporter:
    image: nvcr.io/nvidia/k8s/dcgm-exporter:3.3.8-3.6.0-debian12
    restart: always
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]
    ports:
      - "9400:9400"

  node-exporter:
    image: quay.io/prometheus/node-exporter:latest
    restart: always
    network_mode: host
    pid: host
    volumes:
      - /:/host:ro,rslave
    command:
      - '--path.rootfs=/host'

  prometheus:
    image: prom/prometheus:latest
    restart: always
    network_mode: host
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'

  grafana:
    image: grafana/grafana:latest
    restart: always
    network_mode: host
    volumes:
      - ./grafana:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
cd /opt/monitoring && docker compose up -d

分阶段递进,逐步延长负载时间:

阶段 时长 验证目标
预热 60s VFIO 绑定正常、gpu-burn 能跑起来、所有 GPU 被加载
短时压力 10min 温度能否在合理范围内收敛到稳态
中等负载 1h 长时间满载下的降频行为和功耗墙表现
长时烤机 8-24h 检测间歇性显存错误、PCIe 链路退化、热积累导致的渐进性问题
./gpu_burn 60      # 预热
./gpu_burn 600     # 短时
./gpu_burn 3600    # 中等

# 长时烤机建议放在 tmux 中
tmux new -s gpuburn
./gpu_burn 28800

配合终端侧的实时监控和日志记录:

# 实时观察
watch -n 1 nvidia-smi

# 结构化日志记录
while true; do
    echo "=== $(date) ==="
    nvidia-smi --query-gpu=timestamp,name,gpu_bus_id,temperature.gpu,utilization.gpu,utilization.memory,power.draw,clocks.sm,clocks.mem,memory.used,memory.total,pcie.link.gen.current,pcie.link.width.current --format=csv
    sleep 5
done | tee gpu_burn_log_$(date +%Y%m%d_%H%M%S).csv

# DCGM 完整诊断(在烤机结束后跑一次)
dcgmi diag -r 3

8 小时长烤没有跑完,本文不列温度和功耗数字。复现时按上面的分阶段命令执行,把 nvidia-smi 的 CSV 日志留好;下面是对结果的判定标准。

评估项 通过 关注 不通过
GPU 稳态温度 < 83°C 83-89°C(检查风道/散热片) >= 90°C(过热,必须解决才能投产)
降频行为 无降频 偶发降频 (< 5% 时间) 持续降频 (> 10% 时间)
XID 错误 0 — > 0(硬件或驱动故障)
PCIe Replay 错误 0 1-10(可能是信号抖动) > 10(检查 PCIe riser/线缆/Switch)
gpu-burn FAULTY 0 — > 0(显存或计算单元故障)
功耗限制 偶发(正常行为) 频繁(检查 TDP 配置) 持续(需降低 power limit 或改善供电)

透传跑通、基准测试通过之后,真正的问题才开始:多模型并存时的 GPU 分配、请求路由、访问控制和可观测性。本节直接给出面向生产的方案——以 LiteLLM 网关为统一入口,安全策略内建而非后补。

graph TB
    subgraph Clients["调用方"]
        OPENCLAW["OpenClaw Agent"]
        WEBAPP["Web 应用"]
        API_USER["API 调用方"]
    end

    subgraph Gateway["LLM 网关层 (VLAN 20)"]
        TLS["TLS 终结<br/>Caddy / Nginx"]
        LITE["LiteLLM Proxy<br/>:4000"]
        TLS --> LITE
    end

    subgraph GPU_VM["GPU 推理层 (VLAN 20)"]
        V1["vLLM :8001<br/>Llama-3.1-8B<br/>GPU 0 · fast"]
        V2["vLLM :8002<br/>Qwen2.5-14B<br/>GPU 1 · balanced"]
        V3["vLLM :8003<br/>DeepSeek-R1-32B<br/>GPU 2-3 · reasoning"]
    end

    subgraph Fallback["外部 Fallback"]
        CLAUDE["Claude API"]
    end

    subgraph Observability["可观测性"]
        LANGFUSE["Langfuse<br/>Tracing + 审计"]
        PROM_OBS["Prometheus<br/>+ Grafana"]
    end

    Clients -->|"HTTPS + API Key"| TLS
    LITE --> V1
    LITE --> V2
    LITE --> V3
    LITE -->|"Fallback"| CLAUDE
    LITE -->|"Callback"| LANGFUSE
    GPU_VM --> PROM_OBS

架构要点:

  • 所有调用方只和 LiteLLM 网关通信——vLLM 后端端口不对外暴露,鉴权、限流、审计集中在网关一个点实施
  • TLS 终结在网关前端——API Key 和推理内容不在网络上明文传输
  • vLLM 后端和网关部署在同一 VLAN——调用方(OpenClaw / Web App)在另一个 VLAN,通过防火墙仅放行网关端口

GPU 分配的核心原则:按模型权重和并发需求分卡,而不是平均分。

平台 A(4x RTX 3090,24GB/卡):

GPU 模型 量化 显存占用 定位
GPU 0 Llama-3.1-8B-Instruct FP16 ~16 GB fast — 日常对话、翻译、格式化
GPU 1 Qwen2.5-14B-Chat INT4 (GPTQ) ~8 GB balanced — 内容生成、技术问答
GPU 2-3 DeepSeek-R1-Distill-Qwen-32B INT4 (AWQ), TP=2 ~10 GB/卡 reasoning — 代码审查、复杂推理

平台 B(8x V100-32GB):

GPU 模型 量化 显存占用 定位
GPU 0 Llama-3.1-8B-Instruct FP16 ~16 GB fast
GPU 1-2 Qwen2.5-14B-Chat FP16, TP=2 ~14 GB/卡 balanced
GPU 3-6 Llama-3.1-70B-Instruct INT4 (AWQ), TP=4 ~10 GB/卡 reasoning
GPU 7 Embedding / Reranker FP16 ~4 GB embedding — RAG 场景

在生产环境或没有流畅外网的机房里,不建议让 vLLM 在启动时现拉模型图层。更稳妥的做法是提前将模型下载到本地。特别是像 Llama-3.1 这样的受限模型(Gated Model),必须先在 Hugging Face 官网申请通过并提供 HF_TOKEN 才能下载。

  1. 在 Hugging Face 获取 Access Token(Role 设为 Read 即可)
  2. 注入环境变量并使用原生工具大并发下载:
# 安装 HF 官方工具包
pip install -U "huggingface_hub[cli]"

# 环境变量注入
export HF_TOKEN="hf_xxxxx_your_token_xxxxx"

# 使用国内镜像站加速(避免 huggingface.co 连接超时)
export HF_ENDPOINT="https://hf-mirror.com"

# 指定模型下载的统一目录(比如挂载的高速 NVMe 盘)
export HF_HOME="/data/llm/models"

# 使用 CLI 高速下载(支持断点续传)
# Llama-3.1 是受限模型,必须有 HF_TOKEN 才能拉取
hf download Qwen/Qwen3.5-9B
hf download Qwen/Qwen3.5-35B-A3B
hf download Qwen/Qwen3-Coder-Next
hf download zai-org/GLM-5
hf  download MiniMaxAI/MiniMax-M2.5
hf  download Qwen/Qwen3.5-122B-A10B

有了本地缓存,即使断网 vLLM 仍能秒级启动。

生产环境下 vLLM 必须使用容器化部署,而非裸 pip install——后者缺乏隔离、版本锁定和进程管理能力。先在 GPU VM 上完成 Docker Engine 和 NVIDIA Container Toolkit 的安装:

# 使用官方安装脚本,通过清华镜像源加速
export DOWNLOAD_URL="https://mirrors.tuna.tsinghua.edu.cn/docker-ce"
curl -fsSL https://get.docker.com | sh

# 安装 NVIDIA Container Toolkit(让 Docker 能访问透传的 GPU)
apt-get update &&  apt-get install -y --no-install-recommends \
   ca-certificates \
   curl \
   gnupg2

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey |  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' | \
     tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.18.2-1
   apt-get install -y \
      nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION}


nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

# 验证 GPU 在容器内可见
docker run --rm --gpus all hub.infra.plz.ac/nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi

使用 Docker Compose 统一管理所有 vLLM 推理实例(仅监听 localhost,不对外暴露):

# /opt/llm-inference/docker-compose.yml
services:
  vllm-fast:
    image: vllm/vllm-openai:latest
    restart: always
    ipc: host
    ports:
      - "127.0.0.1:8001:8000"
    volumes:
      - /data/llm/models:/root/.cache/huggingface
    environment:
      - HF_ENDPOINT=https://hf-mirror.com
      - TRANSFORMERS_OFFLINE=1
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ['0']
              capabilities: [gpu]
    command: >
      --model Qwen/Qwen3.5-9B
      --dtype float16
      --gpu-memory-utilization 0.9
      --max-model-len 4096
      --port 8000
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  vllm-balanced:
    image: vllm/vllm-openai:latest
    restart: always
    ipc: host
    ports:
      - "127.0.0.1:8002:8000"
    volumes:
      - /data/llm/models:/root/.cache/huggingface
    environment:
      - HF_ENDPOINT=https://hf-mirror.com
      - TRANSFORMERS_OFFLINE=1
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ['1']
              capabilities: [gpu]
    command: >
      --model Qwen/Qwen3.5-35B-A3B
      --dtype float16
      --gpu-memory-utilization 0.9
      --max-model-len 4096
      --port 8000
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  vllm-reasoning:
    image: vllm/vllm-openai:latest
    restart: always
    ipc: host
    ports:
      - "127.0.0.1:8003:8000"
    volumes:
      - /data/llm/models:/root/.cache/huggingface
    environment:
      - HF_ENDPOINT=https://hf-mirror.com
      - TRANSFORMERS_OFFLINE=1
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ['2', '3']
              capabilities: [gpu]
    command: >
      --model Qwen/Qwen3.5-122B-A10B
      --dtype float16
      --tensor-parallel-size 2
      --gpu-memory-utilization 0.9
      --max-model-len 8192
      --port 8000
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
cd /opt/llm-inference && docker compose up -d

# 验证所有实例健康
docker compose ps
curl -s http://127.0.0.1:8001/health
curl -s http://127.0.0.1:8002/health
curl -s http://127.0.0.1:8003/health

127.0.0.1:800x:8000 端口映射确保 vLLM 只监听本地回环。restart: always + healthcheck 实现进程级自愈:容器崩掉自动重启,健康检查失败自动标记为 unhealthy 供 LiteLLM 摘除。

LiteLLM 作为统一入口,负责模型路由、API Key 鉴权、限流、Fallback 和审计。

# /opt/llm-gateway/litellm_config.yaml
model_list:
  - model_name: "fast"
    litellm_params:
      model: "openai/meta-llama/Llama-3.1-8B-Instruct"
      api_base: "http://127.0.0.1:8001/v1"
      api_key: "not-used"

  - model_name: "balanced"
    litellm_params:
      model: "openai/Qwen/Qwen2.5-14B-Chat-GPTQ-Int4"
      api_base: "http://127.0.0.1:8002/v1"
      api_key: "not-used"

  - model_name: "reasoning"
    litellm_params:
      model: "openai/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
      api_base: "http://127.0.0.1:8003/v1"
      api_key: "not-used"

  # 外部 Fallback:本地 reasoning 不可用时自动降级
  - model_name: "reasoning"
    litellm_params:
      model: "claude-sonnet-4-20250514"
      api_key: "${ANTHROPIC_API_KEY}"

router_settings:
  routing_strategy: "simple-shuffle"
  num_retries: 2
  timeout: 300
  allowed_fails: 3               # 连续失败 3 次暂时摘除后端

general_settings:
  master_key: "${LITELLM_MASTER_KEY}"
  database_url: "sqlite:///litellm.db"
  allow_user_auth: true

litellm_settings:
  max_budget: 100
  budget_duration: "monthly"
  success_callback: ["langfuse"]  # 请求级 tracing
  store_model_in_db: true         # 审计日志持久化

同名模型 reasoning 定义了两个后端(本地 vLLM + Claude API)。LiteLLM 会优先使用本地实例,连续失败后自动 Fallback 到 Claude。

不同调用方分配不同权限的 Key——不存在一个无限权限的"万能 Key":

# OpenClaw Agent — 三个模型都可用,月预算 $50
curl -X POST http://localhost:4000/key/generate \
  -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
  -d '{
    "key_alias": "openclaw-agent",
    "models": ["fast", "balanced", "reasoning"],
    "max_budget": 50, "budget_duration": "monthly",
    "max_parallel_requests": 5,
    "tpm_limit": 100000, "rpm_limit": 60
  }'

# Web 应用 — 只允许 fast,月预算 $10
curl -X POST http://localhost:4000/key/generate \
  -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
  -d '{
    "key_alias": "webapp-chatbot",
    "models": ["fast"],
    "max_budget": 10, "budget_duration": "monthly",
    "max_parallel_requests": 10,
    "tpm_limit": 50000, "rpm_limit": 100
  }'
控制维度 OpenClaw Key Web App Key
可用模型 fast, balanced, reasoning 仅 fast
月度预算 $50 $10
最大并发 5 10
TPM 限制 100K 50K
RPM 限制 60 100
# Caddy 一行搞定 TLS 终结(自签证书 / ACME)
caddy reverse-proxy --from llm.internal:443 --to localhost:4000

内网有自己 CA 的场景下用 mTLS 做双向认证:

server {
    listen 443 ssl;
    server_name llm.internal;

    ssl_certificate     /etc/ssl/llm-gateway.crt;
    ssl_certificate_key /etc/ssl/llm-gateway.key;
    ssl_client_certificate /etc/ssl/ca.crt;
    ssl_verify_client on;

    location / {
        proxy_pass http://127.0.0.1:4000;
        proxy_read_timeout 600s;
    }
}
graph LR
    subgraph VLAN20["VLAN 20 — AI 推理网络"]
        VLLM["vLLM(仅 127.0.0.1)"]
        LITELLM_GW["LiteLLM Gateway"]
        VLLM ---|"localhost"| LITELLM_GW
    end

    subgraph VLAN30["VLAN 30 — 应用网络"]
        OC["OpenClaw"]
        WA["Web App"]
    end

    subgraph VLAN10["VLAN 10 — 管理网络"]
        ADM["管理终端"]
    end

    OC -->|"仅 :443"| LITELLM_GW
    WA -->|"仅 :443"| LITELLM_GW
    ADM -->|"SSH + Grafana"| VLLM
规则 源 → 目标 端口 说明
允许 VLAN 30 → LiteLLM 443 调用方只能走网关
允许 VLAN 10 → GPU VM 22, 3000, 9090 运维管理
拒绝 VLAN 30 → vLLM 8001-8003 禁止绕过网关
拒绝 * → GPU VM * 默认拒绝

在多模型+Agent的架构中,仅仅有 Prometheus 监控 GPU 硬件层面的指标(利用率、显存、功耗)是不够的。在业务层,你需要知道:每个 Prompt 究竟长什么样、模型回复了什么包、哪一步生成发生了拒绝回答(Refusal)、不同 Agent 的实际 Token 消耗成本是多少。

Langfuse 是目前首选的开源 LLM 可观测性平台,它能和 LiteLLM 网关无缝整合。

使用 Docker Compose 在管理网络(VLAN 10)单独拉起 Langfuse 基础设施:

# docker-compose.langfuse.yml
version: '3.8'
services:
  langfuse-server:
    image: langfuse/langfuse:2
    depends_on:
      - db
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
      - NEXTAUTH_SECRET=my_secure_secret
      - SALT=my_secure_salt
      - NEXTAUTH_URL=http://192.168.1.14:3000
      - TELEMETRY_ENABLED=false
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=postgres
    volumes:
      - langfuse_data:/var/lib/postgresql/data

volumes:
  langfuse_data:

LiteLLM 原生深度集成了 Langfuse。只需向 LiteLLM Gateway 提供凭据,即可自动拦截并上传所有的请求元数据,完全不需要修改 OpenClaw 或 Web 应用层面的代码。

在 LiteLLM 环境变量中注入 Langfuse API 密钥:

# 在启动 LiteLLM前设置(或写入系统 service 文件)
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
# 指向刚部署的 Langfuse 地址
export LANGFUSE_HOST="http://192.168.1.14:3000"

确保 litellm_config.yaml 中启用了回调:

litellm_settings:
  success_callback: ["langfuse"]
  failure_callback: ["langfuse"]

接入后,无论是 Web App 还是 OpenClaw Agent 产生的对话,都能在 Langfuse 控制台获得以下业务级观测能力:

  1. Generation Replay (生成回放):完整查看原始请求头、System Prompt、工具定义(Tool definitions)和最终响应。对于 OpenClaw Agent 场景,Agent 组装的上下文非常复杂,当发生越狱漏洞(Prompt Injection)时,这是极其关键的留档证据。
  2. 多维度耗时拆解 (Latency Breakdown):Langfuse 会将流式请求(Streaming)精确拆解为 Time-to-First-Token (首字延迟 TTFT) 和 Tokens-per-Second (生成速度)。如果首字延迟异常飙升,往往说明 vLLM PagedAttention K-V Cache 碎化严重或并发队列在排队;如果生成速度骤降,可以通过回放观察是不是命中了复杂的推理生成。
  3. 用户粒度成本聚合 (Cost by User/Key):由于前文为 OpenClaw 和 WebApp 分配了不同的 LiteLLM Key,Langfuse 自动将 Key ID 映射归类。你可以直观看到不同调用方每日"吃掉"了多少请求、占用多少 Token,配合预算体系实现闭环 FinOps 管理。

上面的方案是"每模型独占 GPU,常驻内存"——适合模型数量 <= GPU 数量的场景。当模型数量超过 GPU 数量时,需要动态调度。

同基座模型 + 多 LoRA 适配器的场景下,vLLM 原生支持 LoRA 热切换,无需多实例:

python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-8B-Instruct \
    --enable-lora \
    --lora-modules \
        code-assistant=./lora-adapters/code \
        translator=./lora-adapters/translator \
        summarizer=./lora-adapters/summarizer \
    --max-loras 3 --max-lora-rank 16 \
    --host 127.0.0.1 --port 8001

LoRA 适配器只有几十 MB,切换几乎零开销。适合企业内部多部门共用一个基座模型但需要不同领域能力的场景。

GPU 数量够多(如 8x V100)且需要精细化资源管理时:

# serve_app.py
from ray import serve
from vllm import LLM, SamplingParams

@serve.deployment(num_replicas=1, ray_actor_options={"num_gpus": 1})
class LLMDeployment:
    def __init__(self, model_name: str):
        self.llm = LLM(model=model_name, dtype="float16", gpu_memory_utilization=0.9)

    async def generate(self, prompt: str, max_tokens: int = 512):
        params = SamplingParams(max_tokens=max_tokens)
        outputs = self.llm.generate([prompt], params)
        return outputs[0].outputs[0].text

light = LLMDeployment.bind("meta-llama/Llama-3.1-8B-Instruct")
heavy = LLMDeployment.bind("deepseek-ai/DeepSeek-R1-Distill-Qwen-32B")

Ray Serve 支持 GPU 分数分配(如 0.5 GPU 跑小模型)、按请求量自动扩缩、跨节点调度。代价是引入 Ray 集群的运维复杂度——4 卡场景不值得,8 卡以上可以考虑。

将上面的推理集群接入 OpenClaw,实现按任务复杂度自动路由模型。

graph TB
    subgraph Access["消息通道"]
        TG["Telegram Bot"]
        WEB["Control UI"]
    end

    subgraph Agent["OpenClaw (VLAN 30)"]
        GW["Gateway :18789"]
        PI["Agent 实例"]
        SANDBOX["Docker Sandbox"]
        SKILL["Skills<br/>model-router"]
    end

    subgraph LLM["LLM 网关 (VLAN 20)"]
        LITE["LiteLLM :443<br/>TLS + Key 鉴权"]
    end

    subgraph GPUs["GPU 推理 (VLAN 20)"]
        F["fast<br/>8B"]
        B["balanced<br/>14B"]
        R["reasoning<br/>32B"]
    end

    TG --> GW
    WEB --> GW
    GW --> PI
    PI --> SANDBOX
    PI --> SKILL

    PI -->|"sk-openclaw-xxx"| LITE
    LITE --> F
    LITE --> B
    LITE --> R
{
  "models": {
    "providers": {
      "gpu-cluster": {
        "type": "openai-compatible",
        "baseUrl": "https://llm.internal/v1",
        "apiKey": "${LITELLM_API_KEY}",
        "models": [
          { "id": "fast",      "name": "快速响应 (8B)",  "contextLength": 4096 },
          { "id": "balanced",  "name": "均衡 (14B)",     "contextLength": 4096 },
          { "id": "reasoning", "name": "深度推理 (32B)", "contextLength": 8192 }
        ]
      }
    }
  },
  "agents": {
    "defaults": {
      "model": "gpu-cluster:fast"
    }
  },
  "tools": {
    "profile": "messaging",
    "deny": ["group:automation", "group:runtime"],
    "exec": { "security": "deny", "ask": "always" },
    "elevated": { "enabled": false }
  },
  "channels": {
    "telegram": {
      "dmPolicy": "pairing",
      "groups": { "*": { "requireMention": true } }
    }
  }
}

通过 Skill 让 Agent 根据任务性质自动切换模型,用户无需手动 /model:

<!-- ~/.openclaw/skills/model-router/SKILL.md -->
---
name: model-router
description: 根据任务复杂度自动选择 LLM 模型
---

## 模型选择规则

根据以下规则选择模型,切换前无需询问用户:

1. **简单任务**(翻译、格式转换、日常对话、摘要)→ `gpu-cluster:fast`
2. **中等任务**(技术问答、内容创作、文档编写)→ `gpu-cluster:balanced`
3. **复杂任务**(代码审查、数学推理、架构设计、多步分析)→ `gpu-cluster:reasoning`

切换时使用 `/model` 命令。如果不确定复杂度,默认使用 `balanced`。

多模型 + Agent 场景下的威胁模型是 prompt injection → 模型越权 → 命令执行。三层防御缺一不可:

防御层 实施点 措施
模型权限 LiteLLM Key models 白名单——Agent 只能调用被授权的模型
执行权限 OpenClaw tools exec.security: "deny" + elevated: false——即使推理结果含危险命令也不执行
资源兜底 LiteLLM Key max_budget + rpm_limit——prompt 注入导致的循环调用有上限
审计追溯 LiteLLM + Langfuse + OpenClaw Session 完整请求链路:谁 → 哪个 Key → 哪个模型 → 多少 Token
网络隔离 VLAN + 防火墙 Agent 无法绕过网关直连 vLLM,攻击面收窄到 API 层

即使攻击者成功注入恶意 prompt 让 Agent 切换到 reasoning 模型并生成了 rm -rf / 指令:LiteLLM 的 RPM 限制阻止了批量请求;OpenClaw 的 exec.security: "deny" 拦截了命令执行;max_budget 确保即使跑了一夜也只花 $50。

症状: VM 内 nvidia-smi 报 No devices found 或 NVIDIA-SMI has failed

排查路径:

# 1. 宿主机侧:确认 VFIO 绑定
lspci -nnk -s <BDF>
# 期望: Kernel driver in use: vfio-pci

# 2. 宿主机 dmesg
dmesg | grep -i vfio
dmesg | grep -i nvidia

# 3. VM 配置
grep hostpci /etc/pve/qemu-server/<VMID>.conf

常见根因(按频率排序):

  1. NVIDIA 驱动在宿主机上先于 VFIO 加载了 GPU——检查 initramfs 中的模块加载顺序
  2. IOMMU 分组问题——同组内有其他设备未被绑定
  3. Above 4G Decoding 未开启——BAR 分配失败
  4. ReBAR 导致的 Code 43——关闭 BIOS 中的 Resizable BAR

这不是 Bug,这是预期行为。没有 framebuffer 设备自然没有画面。

解法:

  • IPMI:最可靠的带外管理
  • SSH:保证网络配置正确即可
  • PVE Web UI:https://<PVE_IP>:8006

症状: 透传 4 张 GPU,VM 内只识别到 2-3 张

# 逐张确认 VFIO 状态
for dev in $(lspci -nn | grep NVIDIA | awk '{print $1}'); do
    echo "=== $dev ==="
    lspci -nnk -s $dev
done

大概率是 IOMMU 分组问题,或者是 VM 配置文件中 hostpci 地址写错了。另外检查 qm.conf 中是否所有 hostpci 行都被正确解析(PVE 有时会因为格式问题静默忽略某些行)。

症状: VM 关机后重启,GPU 无法初始化,必须重启宿主机

NVIDIA GPU 的 FLR(Function Level Reset)实现并不完善,部分型号在 VM 关机后无法干净地重置回初始状态。缓解措施:

  • 使用 vendor-reset 内核模块(AMD GPU 有效,NVIDIA 场景有限)
  • PVE 内核 6.x+ 对 NVIDIA FLR 有改善
  • VM 关机使用 ACPI shutdown(QEMU Guest Agent)而非 stop/reset

排查思路(从低到高):

  1. PCIe 链路降级:lspci -vvv | grep -i lnksta 确认链路速度和宽度是否符合预期
  2. NUMA 亲和性错误:GPU 在 NUMA Node 0,VM 的 vCPU 绑定在 Node 1——跨 NUMA 的内存访问延迟翻倍
  3. Hugepages 未启用:QEMU 默认用 4K 页,TLB miss 率高
  4. 宿主机 CPU 调度器:检查 scaling_governor 是否为 performance

GPU 透传性能的关键在于数据路径最短——GPU、内存、CPU 必须在同一个 NUMA 域内。

# 查看 GPU 所属 NUMA 节点
cat /sys/bus/pci/devices/0000:41:00.0/numa_node

# 查看系统 NUMA 拓扑
numactl -H

VM 配置:

# /etc/pve/qemu-server/<VMID>.conf
cpu: host
cores: 16
numa: 1
cpuunits: 10000

理想状态是 vCPU pin 到 GPU 同侧的物理核上,内存也从同一 NUMA 节点分配。这在双路平台上尤为关键——跨路访问的延迟惩罚通常在 50-100ns。

# 宿主机
echo "vm.nr_hugepages = 16384" >> /etc/sysctl.conf
sysctl -p

# VM 配置
# /etc/pve/qemu-server/<VMID>.conf
hugepages: 1024

Hugepages 减少了 QEMU 进程的 TLB miss,对大内存 VM(GPU 工作负载通常分配 64-128 GB)效果显著。

pcie_acs_override=downstream,multifunction 在 GRUB 参数中已配置。关于它的安全影响:ACS Override 绕过了 PCIe 的 Access Control Services,意味着同一 PCIe Switch 下的设备之间不再有硬件级的 DMA 隔离。在单租户(所有 GPU 都给同一个 VM)场景下这完全可以接受;在多租户场景下需要评估风险。

这轮做完,4 卡 RTX 3090 平台在 PVE 下完成了透传配置、VFIO 绑定、PyTorch 训练和 vLLM 推理的实测;V100 平台记录了配置差异,性能数据还没采集,不能拿它和 3090 做数字对比。

  • RTX 3090 平台(无板载显卡):保留 SSH + Web 管理,4 张卡直通给同一台 Debian 13 VM 可行;单卡 ResNet-50 约 512 images/sec,4 卡 DDP 总吞吐约 1820 images/sec,线性扩展比 0.89。
  • V100 平台(双路 NUMA):配置差异已经梳理,IOMMU 分组和 NUMA 绑定的注意事项写在上面;8 卡训练、推理和长时烤机数据还没采集,后续补。

透传本身在当前内核(6.x+)和 QEMU 下已经比较成熟,但“能跑”和“能稳定跑”是两回事。IOMMU 分组、NUMA 亲和性、Hugepages 和监控要一起配;比起追求最后一点带宽损耗,先把可观测性和模型网关做稳更有价值。

  • vGPU 方案评估(NVIDIA vGPU License / SR-IOV MIG)
  • 不同 PVE 内核版本对透传稳定性的影响
  • Windows VM 透传与 Anti-VM Detection
  • K8s GPU Operator 在透传 VM 内的部署体验
  • 补齐 V100 平台的训练、推理和 8 小时烤机数据