上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-07 11:50 阅读量:709


凌晨两点,我在香港机房中盯着一台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就能在生产场景里跑得既稳又快。

目录结构
全文