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

香港机房这台新机器要在 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、内存、网卡,谁也不许跨区抢活。这样做,机器就像拧紧的齿轮,稳、快、可预期。
如果你也在香港的机房里跟我一样折腾,愿这些踩过的坑、写下的命令,能让你少熬几个通宵。祝你那台机器,也顺顺利利地跑起来。