AI推理平台如何在香港服务器的Ubuntu系统中结合GPU直通与TensorRT,优化深度学习推理延迟?

凌晨两点,我在香港机房中盯着一台2U服务器的IPMI界面——客户要求“不中断旧业务,新业务要进虚拟化”,还点名“GPU直通到Ubuntu虚拟机里跑TensorRT,HTTP接口对外,最好p95延迟<8ms(640×640的目标检测)”。香港这边业务链路离用户近,网络没问题,关键是推理延迟。
1. 目标、架构与硬件清单
1.1 目标
- 在Ubuntu 22.04 LTS 宿主机(KVM)上,将 NVIDIA GPU 直通给 Ubuntu 22.04 虚拟机;
- 在虚拟机中使用 TensorRT(容器化)部署模型服务(Triton/自研皆可);
- 在 FP16/INT8 下把 p50/p95 延迟压到目标线以内;
- 服务通过 Nginx 反向代理对外提供 HTTP/gRPC;
- 具备可观测性(DCGM、Prometheus、Grafana)。
1.2 架构图(文字版)
[Internet]
|
[Nginx on VM] --(localhost)--> [Triton/TensorRT Runtime on VM] --GPU--> [Tensor Cores]
|
[Host: Ubuntu 22.04 + KVM/QEMU + VFIO] --PCIe passthrough--> [GPU]
1.3 硬件与软件清单(本次实际)
| 项 | 型号/版本 | 备注 |
|---|---|---|
| 机型 | 2U 单路 AMD EPYC 7443P / 或 Intel Xeon Silver 同级 | NUMA单路更好控核 |
| 内存 | 256GB DDR4 ECC | 预留大页 |
| GPU | NVIDIA L40S 48GB(或 A5000 24GB / A100 80GB) | 需支持重置良好 |
| 网卡 | Mellanox ConnectX-5 25GbE | 低延迟,RDMA非必需 |
| 系统(宿主) | Ubuntu 22.04.4 LTS(5.15/6.x内核均可) | KVM/OVMF |
| 系统(来宾/VM) | Ubuntu 22.04 LTS | 驱动/容器/推理 |
| 虚拟化 | KVM/QEMU + libvirt + OVMF(UEFI) | GPU直通 |
| 推理框架 | TensorRT 10.x(NGC镜像) | Triton可选 |
| 容器 | Docker + nvidia-container-toolkit | 仅在VM内装驱动 |
2. BIOS/固件前置:为直通“腾地”
进入BIOS(IPMI/远程KVM):
- 打开 SVM/VT-x/VT-d/IOMMU(AMD SVM / Intel VT-d)
- 打开 Above 4G Decoding(必开,否则BAR内存不够)
- 能开 Resizable BAR 就开(有些GPU/主板组合可提升带宽映射效率)
- 关闭多余的板载图形,确保独显在独立IOMMU组
小坑:有些主板不开4G Decoding,GPU BAR映射只有256MB,会导致直通后驱动加载诡异失败或性能雪崩。
3. 宿主机(Ubuntu)准备:KVM + VFIO 直通
3.1 安装KVM/OVMF
sudo apt update
sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients virt-manager ovmf
sudo systemctl enable --now libvirtd
3.2 开IOMMU & 预设内核参数
编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 里追加(AMD/Intel 二选一):
AMD:
amd_iommu=on iommu=pt
Intel:
intel_iommu=on iommu=pt
(如果遇到IOMMU组过大影响直通,可尝试 pcie_acs_override=downstream,multifunction,但谨慎使用)
更新GRUB并重启:
sudo update-grub
sudo reboot
3.3 确认IOMMU组 & 识别GPU
# 列出IOMMU组
for d in /sys/kernel/iommu_groups/*/devices/*; do
echo "IOMMU GROUP $(basename $(dirname $d)) - $(lspci -nns $(basename $d))";
done
# 确认GPU及其伴生设备(如音频函数)
lspci -nn | egrep -i "nvidia|vga|3d|audio"
记下 GPU 的 PCI ID(形如 0000:65:00.0)和 供应商/设备ID(形如 [10de:26b5])。
3.4 VFIO绑定(宿主机不装显卡驱动)
创建模块加载与黑名单:
# 让vfio模块常驻
echo -e "vfio\nvfio_pci\nvfio_iommu_type1\nvfio_virqfd" | sudo tee /etc/modules-load.d/vfio.conf
# 黑名单 nouveau(以防万一)
echo -e "blacklist nouveau\noptions nouveau modeset=0" | sudo tee /etc/modprobe.d/blacklist-nouveau.conf
sudo update-initramfs -u
将GPU绑定到 vfio-pci(替换为你的ID):
GPU_IDS="10de:26b5,10de:228e" # 例:显卡/音频函数两个ID
echo "options vfio-pci ids=${GPU_IDS} disable_vga=1" | sudo tee /etc/modprobe.d/vfio-pci.conf
sudo update-initramfs -u
sudo reboot
重启后确认:
lspci -nnk -s 65:00.0 # 替换为你的PCI地址
# Kernel driver in use: vfio-pci <-- 应该看到这个
小坑:如果“Kernel driver in use”仍然是 nvidia 或 nouveau,说明有模块先于vfio抢占了设备驱动。检查initramfs与黑名单、以及vfio-pci.conf的ID是否正确。
4. 创建GPU直通虚拟机(libvirt)
4.1 新建VM基本配置
固件:UEFI (OVMF)
- CPU:host-passthrough,分配 4–8 vCPU(独占核更稳),并 CPU Pinning 到同一NUMA节点
- 内存:32–64GB(根据模型体量),启用 HugePages(1G更好,或2M也可)
- 磁盘:NVMe直通或virtio-blk,尽量把模型与引擎放SSD/NVMe
HugePages(宿主机):
# 例如预留 64 * 1GB = 64GB 大页(根据内存而定)
echo "vm.nr_hugepages=64" | sudo tee /etc/sysctl.d/99-hugepages.conf
sudo sysctl --system
在VM的domain XML里加:
<memoryBacking>
<hugepages/>
</memoryBacking>
CPU绑定(示例片段):
<cputune>
<vcpupin vcpu='0' cpuset='16'/>
<vcpupin vcpu='1' cpuset='17'/>
<vcpupin vcpu='2' cpuset='18'/>
<vcpupin vcpu='3' cpuset='19'/>
</cputune>
4.2 PCI直通(显卡+音频函数都加上)
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x65' slot='0x00' function='0x0'/>
</source>
<driver name='vfio'/>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x65' slot='0x00' function='0x1'/>
</source>
<driver name='vfio'/>
</hostdev>
小坑:多函数设备(function=0/1)要一起直通;IOMMU组里有桥/其他设备混进去时,要么把整组直通,要么更换插槽/主板设置调整ACS,避免“组内不干净”。
5. 来宾(VM)系统:NVIDIA驱动 + TensorRT容器
5.1 VM里安装NVIDIA驱动(仅虚拟机装)
sudo apt update
sudo apt install -y build-essential dkms curl
# 推荐用官方repo安装或使用与TensorRT镜像兼容的版本
sudo apt install -y nvidia-driver-550 # 示例:按需替换
sudo reboot
验证:
nvidia-smi
# 能看到GPU、功耗、驱动版本
5.2 Docker + nvidia-container-toolkit
# Docker
curl -fsSL https://get.docker.com | sudo bash
sudo usermod -aG docker $USER
# NVIDIA Container Toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.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
sudo apt update
sudo apt install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
5.3 选择镜像(两条路线)
- Triton Inference Server(NGC镜像,带TensorRT/ONNXRuntime)
- 纯TensorRT运行时(nvcr.io/nvidia/tensorrt:<tag>-py3)
示例(Triton):
docker run --gpus all --rm -it -p8000:8000 -p8001:8001 -p8002:8002 \
-v /opt/models:/models nvcr.io/nvidia/tritonserver:24.05-py3 \
tritonserver --model-repository=/models --strict-model-config=false
6. 模型转换:ONNX → TensorRT(含INT8校准)
以 YOLOv8n 为例(640×640),演示从 PyTorch → ONNX → TensorRT。
6.1 导出ONNX(在VM内)
python3 -m venv venv && source venv/bin/activate
pip install ultralytics onnx onnxsim
python - <<'PY'
from ultralytics import YOLO
m = YOLO('yolov8n.pt')
m.export(format='onnx', dynamic=True, opset=13)
PY
# 简单做个简化
python - <<'PY'
import onnx
from onnxsim import simplify
model = onnx.load("yolov8n.onnx")
model_simp, check = simplify(model)
assert check, "Simplified ONNX model could not be validated"
onnx.save(model_simp, "yolov8n_simp.onnx")
PY
6.2 用 trtexec 快速生成 FP16/INT8 引擎
FP16:
docker run --gpus all --rm -v $PWD:/work -w /work nvcr.io/nvidia/tensorrt:24.05-py3 \
trtexec --onnx=yolov8n_simp.onnx \
--saveEngine=yolov8n_fp16.plan \
--fp16 \
--workspace=4096 \
--minShapes=images:1x3x640x640 \
--optShapes=images:4x3x640x640 \
--maxShapes=images:8x3x640x640 \
--timingCacheFile=tactic.cache
INT8(校准):准备50~200张代表性图片,写个简单的校准缓存生成脚本(示意)。
# 先用 trtexec 快速校准(需要提供校准数据目录)
docker run --gpus all --rm -v $PWD:/work -w /work nvcr.io/nvidia/tensorrt:24.05-py3 \
trtexec --onnx=yolov8n_simp.onnx \
--saveEngine=yolov8n_int8.plan \
--int8 --fp16 \
--calib=data/calib_images \
--workspace=4096 \
--minShapes=images:1x3x640x640 \
--optShapes=images:4x3x640x640 \
--maxShapes=images:8x3x640x640 \
--timingCacheFile=tactic.cache
经验:INT8 对校准集分布极其敏感。电商、人脸、交通等场景最好分别准备同分布样本。否则延迟虽降了,召回/精度会掉的肉眼可见。
7. 部署:Triton 模型库与 Nginx
7.1 Triton 模型库结构
/opt/models/
yolov8n/
1/
model.plan # TensorRT engine
config.pbtxt
config.pbtxt(示例):
name: "yolov8n"
backend: "tensorrt"
max_batch_size: 8
input [
{ name: "images" data_type: TYPE_FP32 dims: [3, 640, 640] }
]
output [
{ name: "output0" data_type: TYPE_FP32 dims: [N, 84] } # 示例,按实际模型导出调整
]
instance_group [
{ kind: KIND_GPU, count: 1, gpus: [0] }
]
dynamic_batching { preferred_batch_size: [1, 2, 4, 8] max_queue_delay_microseconds: 2000 }
7.2 Docker Compose(VM内)
version: "3.8"
services:
triton:
image: nvcr.io/nvidia/tritonserver:24.05-py3
command: >
tritonserver --model-repository=/models
--strict-model-config=false
--http-port=8000 --grpc-port=8001 --metrics-port=8002
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
volumes:
- /opt/models:/models
ports:
- "8000:8000"
- "8001:8001"
- "8002:8002"
nginx:
image: nginx:1.25
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
network_mode: "host"
nginx.conf(关键段):
worker_processes auto;
events { worker_connections 4096; }
http {
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
server {
listen 80 reuseport;
location /infer {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8000/v2/models/yolov8n/infer;
}
}
}
8. 运行时调优:把延迟一寸寸抠出来
8.1 GPU 设置
# 保持持久化模式
sudo nvidia-smi -pm 1
# 有条件可锁频/设置功耗上限(不同GPU支持不同命令)
# sudo nvidia-smi -pl 300 # 示例:功耗上限
# sudo nvidia-smi -lgc 1800,2100 # 示例:锁定图形频率区间(视GPU支持)
8.2 CPU/内核/NUMA
宿主机:
# CPU性能模式
sudo apt install -y linux-tools-common linux-tools-`uname -r`
sudo cpupower frequency-set -g performance
# 关节能C-States(可在BIOS更稳)
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # Intel示例
虚拟机:
sudo apt install -y tuned
sudo tuned-adm profile latency-performance
Pinning:Nginx、客户端线程与 Triton 进程尽量固定在同一NUMA节点的vCPU,减少跨节点访存。
8.3 网络栈
虚拟机 sysctl:
sudo tee /etc/sysctl.d/99-net-tune.conf <<'EOF'
net.core.somaxconn=4096
net.core.netdev_max_backlog=250000
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=10
net.ipv4.tcp_max_syn_backlog=4096
EOF
sudo sysctl --system
8.4 TensorRT/Triton层面
- 动态批处理:preferred_batch_size 选 [1,2,4,8],并设置 max_queue_delay_microseconds;低延迟场景通常 1~2ms 的队列延迟是上限。
- Engine构建:把常见输入尺寸/批次配在 min/opt/max 里,避免运行时频繁选择不合适的tactic。
- Timing Cache:复用 tactic.cache,升级只需增量 warmup。
- INT8:若精度边界紧,优先 FP16,INT8谨慎上线(灰度/AB)。
9. 压测与对比数据
(同机同环境,YOLOv8n 640×640,单位:毫秒,单请求除非注明)
| ONNX Runtime (CUDA, FP16) | 8.7 | 12.4 | 无特意调度 |
| TensorRT (FP16) | 5.2 | 7.9 | timing cache + 动态批 |
| TensorRT (INT8,良好校准) | 3.9 | 6.1 | 需精度验收 |
| TensorRT (FP16,batch=4,动态批) | 6.8 | 8.2 | QPS↑、p95略升 |
观测指标(Prometheus/DCGM):GPU利用率、SM占用、显存、PCIe Throughput、模型加载时间、HTTP p95/p99、QPS、队列长度。
10. 真实踩坑与应对
IOMMU组“脏”:GPU旁边混了桥或其他设备,直通报错或不稳。
解决:换PCIe槽、打开ACS、或把整组都直通(代价大)。
Above 4G Decoding 没开:TensorRT加载大engine报错或性能骤降。
解决:回BIOS开启,必要时配合Resizable BAR。
宿主机误装NVIDIA驱动:vfio抢不过内核里的nvidia模块。
解决:黑名单+initramfs重建,确认 Kernel driver in use: vfio-pci。
VM关机/重启后GPU不复位:少数显卡/主板组合存在reset bug。
解决:优先选数据中心卡(A系列/L系列),必要时整机断电或升级固件。
INT8精度掉点:校准集与线上分布偏差导致召回下降。
解决:分场景校准、线上灰度、指标对齐(mAP/precision/recall)。
动态批过激:max_queue_delay 设太大,p95飞涨。
解决:低延迟优先,把延迟预算严格压到2ms以内。
NUMA跨节点:VM vCPU跨NUMA,p95随机抖动。
解决:Pin在同一节点;磁盘与网卡也尽量同节点。
模型加载慢:engine很大+冷启动。
解决:预热(curl并发打空请求)、tactic cache、本地NVMe放引擎。
11. 最终交付清单(可复制的小册子)
- BIOS:IOMMU / 4G Decoding / Resizable BAR
- 宿主机:intel_iommu=on iommu=pt 或 amd_iommu=on iommu=pt,VFIO绑定ID
- KVM:UEFI/OVMF,HugePages,CPU Pinning,同NUMA
- VM:NVIDIA驱动、Docker、nvidia-container-toolkit
- TensorRT:ONNX→TRT(FP16为主,INT8按需),tactic cache
- Triton:config.pbtxt 动态批合理,端口 8000/8001/8002
- Nginx:/infer 反代、keepalive
- 调优:nvidia-smi 持久化、CPU performance、net sysctl
- 观测:DCGM Exporter + Prometheus + Grafana
12. 附:Triton 客户端示例(Python)
import requests, json, base64
import numpy as np
import cv2
def preproc(img_path):
img = cv2.imread(img_path)
img = cv2.resize(img, (640, 640))
img = img[:, :, ::-1].transpose(2,0,1).astype(np.float32)/255.0
return np.expand_dims(img, 0)
X = preproc("test.jpg").tolist()
payload = {
"inputs": [{
"name": "images",
"shape": [1,3,640,640],
"datatype": "FP32",
"data": X
}]
}
r = requests.post("http://<VM_IP>/infer", json=payload, timeout=2)
print(r.status_code, r.text[:2000])
做完最后一次压测,p95稳在 7ms 多一点,我把 tactic.cache 备份好、Grafana看板挂上阈值报警。收拾完工具走出冷通道,凌晨四点,香港的路边摊刚开火,我拎着包在门口喝了第一口热奶茶——这口是低延迟的味道。不是玄学,也不是黑箱:一步步把系统打实、把每个延迟“黑洞”填平,GPU直通与TensorRT就能在生产场景里跑得既稳又快。