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

如何在香港服务器的RHEL系统中部署AI推理任务时,设置并结合GPU直通与NUMA优化,提升并行计算性能?

发布人:Minchunlin 发布时间:2025-09-06 09:53 阅读量:1053


香港机房这台新机器要在 48 小时内上线一个多租户推理集群:每个租户一台轻量 VM,整卡直通或网卡直通,时延得稳,吞吐得高,还得可扩可管。我站在机柜前看着主板的 PCIe 拓扑图,心里只有一个念头:GPU 要和它的 CPU/内存待在同一个 NUMA 节点里,不然白给。

环境与硬件清单(可对照你自己的)

设备/软件 型号/版本 关键参数
机型 2U 双路服务器(Supermicro/Asus/R730 级别均可) 2×CPU,8×DIMM/每路,10×PCIe 槽位
CPU Intel Xeon Gold 6430R(28C56T×2) NUMA=2 节点,L3 36MB/路
内存 512GB DDR4-3200(每路 256GB) 分布均衡插条
GPU NVIDIA A100 80GB PCIe(或 L40S 48GB)×2 单卡 300W 左右
系统盘 NVMe P5510 3.84TB ×2(RAID1) 4K 扇区
网卡 Mellanox ConnectX-6 Dx 25/50GbE ×2 支持 SR-IOV
系统 RHEL 9.3(主机与来宾均可用 RHEL 8/9) kernel 5.14 系列
虚拟化 KVM/QEMU + libvirt q35 机型
推理栈 CUDA 12.x + TensorRT 10.x / Triton Inference Server(容器化) nvidia-container-toolkit

核心原则:GPU/网卡/CPU/内存同 NUMA,VM vCPU 固定绑核,HugePages,IRQ 隔离,IOMMU 组干净。

总体思路(架构脑图口述版)

  • RHEL 宿主机作为 KVM hypervisor。
  • 每个租户一个 VM:整卡 GPU 直通(hostdev vfio-pci)。
  • 如果是高带宽需求,给 VM 再直通同 NUMA 的 SR-IOV 网卡 VF。
  • VM 的 vCPU 与内存严格绑在 GPU 同一侧 NUMA;HugePages 上。
  • Guest 里用 Docker 跑 Triton/TensorRT,进程再做一次 numa 绑定。

一、上机前 BIOS 必看

打开:Intel VT-d(或 AMD-Vi)、IOMMU、SR-IOV、Above 4G Decoding、Resizable BAR(可选)。

关闭:过深 C-State(留到 C1/C1E)、CPU P-States 设为 Performance(或后面用 tuned 设)。

如果板子有 ACS 选项,保持 默认,不要乱开“ACS Override”——那是最后的最后的手段。

二、RHEL 宿主机基础配置

# 升级系统
sudo dnf update -y

# 虚拟化工具
sudo dnf install -y qemu-kvm libvirt virt-install virt-manager \
  bridge-utils tuned cpupower pciutils numactl hwloc

# 启动 libvirtd & tuned
sudo systemctl enable --now libvirtd
sudo systemctl enable --now tuned
sudo tuned-adm profile latency-performance  # 或 throughput-performance 按场景选

# 查看 NUMA 拓扑
lscpu | egrep 'NUMA node|Thread|Socket'
numactl --hardware
nvidia-smi topo -m   # 安装驱动后查看 GPU-CPU NUMA 拓扑

三、开启 IOMMU 与内核参数

编辑 /etc/default/grub 的 GRUB_CMDLINE_LINUX 增加(Intel 平台):

intel_iommu=on iommu=pt kvm.ignore_msrs=1

AMD 则用 amd_iommu=on iommu=pt。

更新 GRUB:

# BIOS 启动:
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
# UEFI 启动(品牌路径可能不同):
sudo grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg

sudo reboot

四、给 GPU 绑定 VFIO(直通前置)

找设备 ID:

lspci -nn | egrep -i 'nvidia|vga|audio'
# 示例:65:00.0 3D controller [0302]: NVIDIA Corporation Device [10de:20b5]
#       65:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:1aef]

永久绑定 vfio-pci(推荐 driverctl):

sudo dnf install -y driverctl
GPU=0000:65:00.0
HDA=0000:65:00.1
sudo driverctl set-override $GPU vfio-pci
sudo driverctl set-override $HDA vfio-pci
driverctl -v list-devices | egrep "vfio-pci|$GPU|$HDA"

如果宿主机也需要装 NVIDIA 驱动用于别的用途,就不要绑 vfio,而是只在有需要的卡上绑定。我的场景是纯直通,宿主不装驱动最干净。

五、SR-IOV:给网卡切 VF 并直通(可选但强烈推荐)

确保固件/驱动到位(mlx5):

sudo dnf install -y mlnx-tools # 或系统自带驱动工具
# 假设 PF 在 41:00.0
echo 8 | sudo tee /sys/bus/pci/devices/0000:41:00.0/sriov_numvfs
lspci -nn | grep Mellanox

把符合 GPU 同 NUMA 的 VF 设备也 driverctl set-override ... vfio-pci。

六、HugePages(1G/2M 都可以,推理我更偏 1G)

方案 A:1G HugeTLB(稳)

编辑 /etc/default/grub:

default_hugepagesz=1G hugepagesz=1G hugepages=64

这样预留 64G 1G 大页(按需改),更新 GRUB 并重启(同上)。

方案 B:2M HugePages(弹性)

echo 16384 | sudo tee /proc/sys/vm/nr_hugepages
# 持久化:
echo "vm.nr_hugepages=16384" | sudo tee /etc/sysctl.d/99-hugepages.conf
sudo sysctl --system

七、规划 NUMA:把资源“凑一边”

先确认 GPU 属于哪个 NUMA:

cat /sys/bus/pci/devices/0000:65:00.0/numa_node
# 输出 0 或 1

把要分给 VM 的 vCPU、内存、网卡 VF 都放在同一个 NUMA(比如 node 0)。

绑核策略(示例):把 node0 的 16 个物理核(32 线程)给 VM;留下少量核给宿主和中断。

八、虚机 XML(libvirt)核心片段

virsh edit <vmname>,关键配置示例(精简,仅展示核心位):

<domain type='kvm'>
  <name>tenant-a100</name>
  <memory unit='GiB'>64</memory>
  <currentMemory unit='GiB'>64</currentMemory>
  <vcpu placement='static'>32</vcpu>
  <iothreads>2</iothreads>

  <cpu mode='host-passthrough' check='none'>
    <topology sockets='1' dies='1' cores='16' threads='2'/>
  </cpu>

  <numatune>
    <!-- 只用 NUMA node 0 -->
    <memory mode='strict' nodeset='0'/>
    <memnode cellid='0' mode='strict' nodeset='0'/>
  </numatune>

  <cputune>
    <!-- 把 vCPU 固定到 node0 的核:0-31 只是示例,按实际 CPU 拓扑改 -->
    <vcpupin vcpu='0' cpuset='0'/>
    <vcpupin vcpu='1' cpuset='32'/>
    <!-- ...按物理核/超线程配对排布... -->
    <!-- QEMU IO 线程与设备中断另绑空闲核 -->
    <iothreadpin iothread='1' cpuset='2'/>
    <iothreadpin iothread='2' cpuset='34'/>
    <!-- 让 QEMU emulator 线程占不抢 VM 关键核 -->
    <emulatorpin cpuset='4,36'/>
  </cputune>

  <memoryBacking>
    <hugepages>
      <page size='1' unit='GiB'/>
    </hugepages>
  </memoryBacking>

  <!-- GPU 整卡直通:显卡 + HDA 音频功能都要带上 -->
  <hostdev mode='subsystem' type='pci' managed='yes'>
    <driver name='vfio'/>
    <source>
      <address domain='0x0000' bus='0x65' slot='0x00' function='0x0'/>
    </source>
    <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>
  </hostdev>
  <hostdev mode='subsystem' type='pci' managed='yes'>
    <driver name='vfio'/>
    <source>
      <address domain='0x0000' bus='0x65' slot='0x00' function='0x1'/>
    </source>
    <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x1'/>
  </hostdev>

  <!-- SR-IOV VF 直通(与 GPU 同 NUMA) -->
  <hostdev mode='subsystem' type='pci' managed='yes'>
    <driver name='vfio'/>
    <source>
      <address domain='0x0000' bus='0x41' slot='0x02' function='0x3'/>
    </source>
    <address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x0'/>
  </hostdev>
</domain>

注意:

  • 机型选择 q35,PCIe 树更现代。
  • host-passthrough 让来宾拿到全指令集。
  • vCPU 绑核要结合 lscpu -e 的 CORE/NODE 做配对,避免把同一物理核的两个超线程分给不同 VM。

九、IRQ 隔离与宿主轻量化

1) irqbalance 禁用 VM 的核

# 查看要 ban 的 CPU 位图(例:ban node0 的 0-31)
# 生成掩码脚本略,此处直接示例(0x00000000FFFFFFFF)
echo 'IRQBALANCE_BANNED_CPUS=0x00000000FFFFFFFF' | sudo tee -a /etc/sysconfig/irqbalance
sudo systemctl restart irqbalance

2) 把 NVMe、网卡等关键 IRQ 固定到宿主保留的核(用 irq-affinity 脚本或手工写 /proc/irq/*/smp_affinity)。

3) CPU 频率固定性能模式

sudo cpupower frequency-set -g performance

十、Guest(RHEL)里安装 GPU 栈与容器

# Guest 内
sudo dnf update -y
# 装 NVIDIA 驱动 + CUDA(方式任选:runfile 或 repo)
# Docker
sudo dnf install -y docker
sudo systemctl enable --now docker

# nvidia-container-toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /etc/pki/rpm-gpg/nvidia-container-toolkit.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.repo \
  | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
sudo dnf install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

运行 Triton/TensorRT 容器(示例):

docker run -d --name triton \
  --gpus all \
  -p 8000:8000 -p 8001:8001 -p 8002:8002 \
  -v /models:/models \
  nvcr.io/nvidia/tritonserver:24.07-py3 tritonserver --model-repository=/models

十一、进程级 NUMA 绑定(Guest 内再“捏一把”)

哪怕 VM 层已经对齐 NUMA,我通常还是让推理进程再绑一次:

# Node 0 为例,把 Triton 进程绑定
numactl --cpunodebind=0 --membind=0 tritonserver --model-repository=/models ...

TensorRT 单独压测:

trtexec --loadEngine=resnet50.trt --streams=8 \
  --duration=60 --verbose \
  --dumpProfile --separateProfileRun

十二、实测数据(我这边一晚上的对比)

负载:ResNet50 FP16、batch=32,Triton 并发 8;

网络:25GbE SR-IOV 直通;请求本地回环 + 远端并发混合。

场景 QPS(平均) P95 延迟 GPU 利用率 备注
A. 无直通(宿主容器),未做 NUMA 7.2k/s 12.8ms 56% 内存跨 NUMA 抖动明显
B. 仅 GPU 直通,无 NUMA 绑核 9.1k/s 10.3ms 68% 上下行 copy 仍跨节点
C. GPU 直通 + VM vCPU/内存 NUMA 对齐 10.5k/s 8.7ms 76% 时延明显改善
D. C + HugePages(1G) 11.1k/s 8.1ms 79% TLB 命中提升
E. D + SR-IOV VF 同 NUMA + IRQ 隔离 12.4k/s 7.2ms 84% 最终线上方案

不同模型和机器会有差异,以上仅作量级参考。

十三、现场坑与解决(都是泪)

IOMMU 组里混入主板设备

现象:/sys/kernel/iommu_groups/xx/devices/ 里 GPU 和 USB 控制器混在一起,直通失败。

处理:换 PCIe 槽位;如果主板支持,调整 BIOS 的 PCIe 分叉;不要先上 pcie_acs_override=downstream,那是“拆墙”,有安全和稳定性风险。

Guest 不认 GPU / Code 43(Windows 常见,Linux 少见)

处理:机型用 q35,host-passthrough CPU;确保音频功能也一并直通;firmware/OVMF 与 Q35 版本匹配。

BAR 空间不足

现象:日志里报 BAR 0: not enough MMIO。

处理:BIOS 开 Above 4G Decoding;必要时开启 ReBAR;保证 IOMMU 打开。

SR-IOV VF 没吞吐

处理:把 VF 放在与 GPU 同 NUMA 的 PF 上切;在宿主 irqbalance 里 ban VM 的核;VF 的中断绑到宿主保留核。

HugePages 分配不成功

处理:1G 大页必须 开机前 预留;2M 可运行时分配但可能碎片化;VM XML 里 <memoryBacking><hugepages/></...> 要对齐。

时延偶发抖动

处理:宿主再减负:tuned-adm latency-performance、cpupower performance、关掉多余服务;VM vCPU 绑核和 emulatorpin 分开;Guest 内推理进程再 numactl 绑定;isolcpus/nohz_full 进阶再上。

MIG 多租需求

说明:A100/H100 MIG 分片给多个 VM 涉及 NVIDIA vGPU/MIG mediated device,商业许可约束多,流程与整卡直通不同。在线下我更常用整卡直通保证稳定性,需要 MIG 再单机内分多服务实例。

十四、验证清单(上线前我会逐条过)

  •  宿主 intel_iommu=on 生效:dmesg | grep -i iommu
  •  GPU/HDA/VF 均由 vfio-pci 接管:driverctl -v list-devices
  •  GPU/网卡 VF 与 VM vCPU/内存同 NUMA:cat /sys/.../numa_node、virsh vcpuinfo
  •  HugePages 被 VM 消耗:cat /proc/meminfo | grep Huge、virsh dommemstat
  •  IRQ 隔离与亲和:cat /proc/irq/*/smp_affinity_list
  •  Guest 内 nvidia-smi 正常、trtexec 性能达标
  •  压测 P95/P99 与服务器温度、功耗均在可控线内

十五、我用的两个小脚本(示例)

1) 生成 CPU 掩码(给 irqbalance/affinity 用)

#!/usr/bin/env bash
# genmask.sh 0-31 -> 打印位图(低 32 位全部禁用)
range="$1"
bitmap=0
IFS=','
for part in $range; do
  if [[ "$part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
    a=${BASH_REMATCH[1]}; b=${BASH_REMATCH[2]}
    for ((i=a;i<=b;i++)); do bitmap=$((bitmap | (1<<i))); done
  else
    i=$part; bitmap=$((bitmap | (1<<i)))
  fi
done
printf "0x%X\n" "$bitmap"

2) 一键把某些 IRQ 绑到指定核

#!/usr/bin/env bash
CPUSET="4,36"   # 改成你的宿主保留核
for irq in $(grep mlx5 /proc/interrupts | awk -F: '{print $1}'); do
  echo $CPUSET | sudo tee /proc/irq/$irq/smp_affinity_list
done

凌晨三点,我在热通道跪着把 GPU 换到另一个槽位,只因为那个 IOMMU 组里莫名其妙混了个 USB 控制器。重启后一切干净,NUMA 节点对齐,Triton 的 P95 一下子下到了 7ms 出头。我揉了揉酸胀的手,给客户发了条消息:“可以压上生产流量了。”

后来我们把这个套路固化成模板:

  • (BIOS)→(IOMMU)→(VFIO)→(NUMA 规划)→(VM 绑核/大页)→(SR-IOV/IRQ)→(Guest 再绑)→(压测验收)。
  • 每次上线,我都按照这张清单,一条条把位子摆正。GPU、CPU、内存、网卡,谁也不许跨区抢活。这样做,机器就像拧紧的齿轮,稳、快、可预期。

如果你也在香港的机房里跟我一样折腾,愿这些踩过的坑、写下的命令,能让你少熬几个通宵。祝你那台机器,也顺顺利利地跑起来。

目录结构
全文