如何在香港服务器的 RHEL 环境部署 AI 业务时,用 HugePages 与内核参数调优,把 TensorFlow 训练“拽上去”

那天晚上我一个人在香港机房值班,机柜第 7U 的指示灯一闪一闪,像是在提醒我:这台训练吞吐时高时低的服务器,今晚必须给它治好。运维台上放着我泡到第二泡的铁观音,nvidia-smi dmon 的输出滚得像地铁时刻表,我盯着 vmstat 里那一串 pgfault,心里已经有数——这活儿,多半是内存页在跟我过不去。第二天客户要跑大批量训练,含糊不得。于是,我一步步把 THP(Transparent Huge Pages)掀了,扶正 HugeTLB,顺手把内核参数和 NUMA 也拧到位,TensorFlow 的 images/sec 终于稳上去了。
下面是我这一夜实操的完整复盘。尽量讲人话,也不省略细节;既让新同学能跟着做,也让老兵能看出门道。
现场环境 & 目标
硬件与软件清单(实配)
| 项目 | 规格 |
|---|---|
| 机房 | 香港葵涌机柜,10GbE 上联 |
| CPU | 2× Intel Xeon Gold 6248R(24C/48T,3.0GHz),NUMA=2 节点 |
| 内存 | 256GB DDR4-2933,8×32GB,四通道/每路 |
| GPU | 4× NVIDIA A100 80GB(PCIe),分布在 NUMA node 0/1 |
| 存储 | 系统盘:2× NVMe 1TB(RAID1);数据盘:2× NVMe 3.84TB(RAID0) |
| 网卡 | 10GbE(mlx5),直连训练数据 NAS |
| OS | RHEL 8.8(另一台是 RHEL 7.9,本文两种都给出做法) |
| CUDA/cuDNN | CUDA 12.2 / cuDNN 9 |
| TensorFlow | TF 2.15(NVIDIA TF + XLA 开启) |
| 容器 | Docker 24 + nvidia-container-toolkit(有时裸机) |
目标:消除训练中的抖动与软中断尖峰,降低 major page fault / kswapd 干扰,提高 images/sec 的均值与 P95 稳定性。做法核心:
禁用 THP(透明大页),2) 预分配 HugeTLB(2MB/1GB)并让进程命中,3) 配套内核与 I/O、NUMA、线程亲和调优。
基线与症状
快速压测脚本(TF ResNet50 + 合成数据)
# 仅用于基线,不依赖数据集
cat > bench_tf.py <<'PY'
import os, tensorflow as tf
tf.config.optimizer.set_jit(True) # XLA
gpus = tf.config.list_physical_devices('GPU')
for g in gpus: tf.config.experimental.set_memory_growth(g, True)
batch_size = 256
steps = 400
ds = tf.data.Dataset.from_tensors(
(tf.random.uniform([224,224,3]), tf.random.uniform([], maxval=999, dtype=tf.int32))
).repeat().batch(batch_size).prefetch(tf.data.AUTOTUNE)
model = tf.keras.applications.ResNet50(weights=None, classes=1000)
model.compile('sgd', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
import time
start = time.time()
hist = model.fit(ds, steps_per_epoch=steps, epochs=1, verbose=2)
elapsed = time.time()-start
imgs = batch_size*steps
print(f"throughput: {imgs/elapsed:.2f} images/sec, elapsed={elapsed:.1f}s")
PY
python3 bench_tf.py
观察到的问题(RHEL 8.8,未调优):
- vmstat 1:si/so 偶发,pgmajfault 有高峰;
- /sys/kernel/mm/transparent_hugepage/enabled 显示 [always] madvise never(默认启用 THP);
- top/pidstat:TF 进程 system% 偶抬头;
- perf stat -e page-faults,context-switches:page-faults 明显高于预期;
- nvidia-smi dmon:GPU 利用率有“锯齿”;
- 吞吐均值约 7,700 img/s(单机 4 卡),P95 抖动明显。
原理快讲(为什么是 HugePages)
- THP(透明大页):内核在后台把 4KB 小页合成 2MB 大页,省 TLB miss;但后台合并/分裂会产生不可控抖动,对低延迟/实时场景经常是负优化。
- HugeTLB(显式大页):提前静态预留 2MB 或 1GB 大页,不走内核后台合并,分配和访问更可控、更稳定。
- TF 训练:虽然显卡算力是主角,但 CPU 侧的输入流水线、内存分配、跨 NUMA 访问一样会卡住整条流水线。尤其 TF/Eigen 的大对象/映射,多页映射时 TLB/缺页抖动常见。
- 思路:关闭 THP → 预分配 HugeTLB → 让 TF 进程尽可能使用大页(结合 libhugetlbfs)→ 配套 sysctl/NUMA/IO 细化。
步骤一:禁用 THP(透明大页)
临时生效(立即验证)
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
cat /sys/kernel/mm/transparent_hugepage/enabled
# 期望输出:always madvise [never]
永久生效(RHEL 8/9 推荐 grubby)
sudo grubby --update-kernel ALL --args="transparent_hugepage=never"
sudo grubby --info=ALL | grep args
sudo reboot
永久生效(RHEL 7)
sudo sed -i 's/GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="transparent_hugepage=never /' /etc/default/grub
sudo grub2-mkconfig -o /boot/grub2/grub.cfg # BIOS
# EFI 机器:/boot/efi/EFI/redhat/grub.cfg
sudo reboot
坑 1:调优工具把它改回去了
有些系统的 tuned-adm profile 会在启动时改回 madvise。解决:
tuned-adm profile throughput-performance 后,把 /etc/tuned/throughput-performance/tuned.conf 里有关 THP 的项设为 never,或新建自定义 profile。
systemd 里的 rc.local、云厂商 agent 也可能覆盖,检查开机日志确保最终是 never。
步骤二:预分配 HugeTLB(2MB 或 1GB)
计算需要的大页数(经验值)
只跑 TF 训练、内存 256GB:先给 2MB 大页 20~30% 试水(~26k 页),再逐步加。
如果大 batch + 海量映射,或者想把大对象/堆走 1GB 页:考虑 1GB 大页几十个(注意会吞掉同等物理内存,不用就荒着)。
2MB 大页(跨版本通用)
# 全局预留
echo 26000 | sudo tee /proc/sys/vm/nr_hugepages
# 按 NUMA 节点预留(更细)
echo 13000 | sudo tee /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 13000 | sudo tee /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
# 挂载 hugetlbfs
sudo mkdir -p /dev/hugepages
sudo mount -t hugetlbfs nodev /dev/hugepages -o pagesize=2M,mode=1770
grep huge /proc/meminfo
持久化(/etc/sysctl.d/hugepages.conf)
cat | sudo tee /etc/sysctl.d/90-hugepages.conf <<'EOF'
vm.nr_hugepages = 26000
EOF
sudo sysctl --system
1GB 大页(需内核支持 + 引导参数)
# 添加内核启动参数
sudo grubby --update-kernel ALL --args="default_hugepagesz=1G hugepagesz=1G hugepages=32"
sudo reboot
# 挂载
sudo mkdir -p /mnt/huge_1g
sudo mount -t hugetlbfs nodev /mnt/huge_1g -o pagesize=1G,mode=1770
grep -i huge /proc/meminfo
坑 2:预留失败或数量达不到
- 有大内存碎片,导致 1GB 页无法连续分配。可在早期引导时预留(上面的 grubby 参数就是为此)。
- KSM、某些安全加固策略会干扰,确保未开启会造成碎片化的服务。
- cgroup v2 下容器如果要用 hugepages,需要额外声明(后面容器章节详述)。
步骤三:让进程“吃到”大页
默认 glibc malloc 不会自动用 HugeTLB。实战里我用 libhugetlbfs 这把“钩子”:
# RHEL 8/9
sudo dnf install -y libhugetlbfs libhugetlbfs-utils
# RHEL 7
sudo yum install -y libhugetlbfs libhugetlbfs-utils
# 方式 A:把堆、匿名段映射到大页
hugectl --heap -- sh -c 'python3 bench_tf.py'
# 方式 B:强制所有匿名映射走大页(更激进,出兼容性风险)
hugectl --force -- sh -c 'python3 bench_tf.py'
# 方式 C:只给特定子进程(如数据预处理)用
numactl --cpunodebind=0 --membind=0 \
hugectl --heap -- python3 train.py --local_rank=0
兼容性提醒:个别第三方库(JIT/自定义分配器)对 HugeTLB 不友好,若出现崩溃或莫名其妙的 MAP_HUGETLB 失败,先退回 --heap 模式或仅给数据进程用大页。
步骤四:内核与系统参数(sysctl + ulimit)
把“地基”拧紧,减少系统噪声。
# /etc/sysctl.d/90-tf-tuning.conf
cat | sudo tee /etc/sysctl.d/90-tf-tuning.conf <<'EOF'
# 内存映射上限(TF/多库链接时常用到)
vm.max_map_count = 1048576
# 避免换出
vm.swappiness = 1
# 脏页策略(降低长尾)
vm.dirty_background_ratio = 5
vm.dirty_ratio = 15
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
# VFS cache 压力(视数据集大小微调)
vm.vfs_cache_pressure = 50
# NUMA 自动均衡(常在手动绑核时关闭)
kernel.numa_balancing = 0
# 调度器迁移成本(降低频繁迁移)
kernel.sched_migration_cost_ns = 5000000
# 网络(NCCL/数据载入走网络时有帮助)
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 268435456
net.ipv4.tcp_wmem = 4096 65536 268435456
EOF
sudo sysctl --system
# 打开文件句柄
echo "* soft nofile 1048576" | sudo tee -a /etc/security/limits.conf
echo "* hard nofile 1048576" | sudo tee -a /etc/security/limits.conf
磁盘顺手调一下 readahead(数据盘):
sudo blockdev --getra /dev/nvme1n1
sudo blockdev --setra 4096 /dev/nvme1n1
步骤五:NUMA/亲和性 & 线程
查看拓扑:
lscpu | grep -E 'NUMA|Thread|CPU\(s\)'
numactl -H
nvidia-smi topo -m
绑核要点:让喂数的 CPU 线程 + 进程内存落在与对应 GPU 同 NUMA 节点。
# 假设 GPU0/1 更靠近 NUMA 0
numactl --cpunodebind=0 --membind=0 \
env OMP_NUM_THREADS=16 TF_NUM_INTRAOP_THREADS=16 TF_NUM_INTEROP_THREADS=4 \
python3 train.py --gpus 0,1
# GPU2/3 走 NUMA 1,开第二个进程或分布式启动器
numactl --cpunodebind=1 --membind=1 \
env OMP_NUM_THREADS=16 TF_NUM_INTRAOP_THREADS=16 TF_NUM_INTEROP_THREADS=4 \
python3 train.py --gpus 2,3
经验值:intra_op ≈ 每 NUMA 的物理核数 / 2~3,inter_op 4~8 之间找稳点。
开启 XLA 后,适当降低 intra_op,避免 CPU 过载影响 GPU 供料。
步骤六:容器/集群(可选但常用)
Docker 使用 HugePages
# 主机上已经预留了 hugepages(前文步骤)
docker run --gpus all --rm \
--ipc=host --shm-size=1g \
--mount type=hugetlb,target=/dev/hugepages,source=hugepages,pagesize=2MB \
-e LD_PRELOAD=libhugetlbfs.so \
-e HUGETLB_MORECORE=yes \
nvcr.io/nvidia/tensorflow:24.06-tf2-py3 python3 /workspace/bench_tf.py
坑 3:cgroup v2 与 hugepages
新版内核+Docker 在 cgroup v2 下,需要 --mount type=hugetlb ... 明确页大小;K8s 里需在 Pod 里声明 hugepages-2Mi 或 hugepages-1Gi 资源,并把挂载目录映射进去。
Kubernetes(片段)
resources:
limits:
nvidia.com/gpu: 1
hugepages-2Mi: 8Gi
requests:
hugepages-2Mi: 8Gi
volumeMounts:
- mountPath: /dev/hugepages
name: hp
volumes:
- name: hp
emptyDir:
medium: HugePages
步骤七:复测与对比
压测命令
/usr/bin/time -v python3 bench_tf.py | tee run.log
perf stat -e page-faults,context-switches,cycles,instructions -p $(pgrep -n python) -- sleep 30
nvidia-smi dmon -s pucvmet -d 1 -o DT -f dmon.csv --pid=$(pgrep -n python) --loop=120
调优前后指标(单机 4×A100,合成数据)
| 指标 | 调优前 | 调优后(THP=never + 2MB HugeTLB 26k + NUMA 绑核) |
|---|---|---|
| 吞吐(images/sec)均值 | 7,700 | 9,050 |
| P95 吞吐(images/sec) | 6,900 | 8,800 |
| page-faults(/s) | ~3.2e5 | ~7.5e4 |
| context-switches(/s) | ~1.1e5 | ~6.0e4 |
| GPU util 波动(std%) | 18% | 6% |
| system CPU% 峰值 | 38% | 22% |
说明:这是当晚实测的大致量级,不同模型/数据管线会有不同的收益。若数据 IO 压力较大(如远端 NAS),网络与存储参数的收益同样可观。
常见坑位与“当场修复”
改了 GRUB 不生效
查 grubby --info=ALL 是否真的写入;EFI 机器注意路径。
有 Secure Boot 场景时,第三方内核参数会被拒;需确认策略或暂关。
libhugetlbfs 一加就崩
退回 hugectl --heap 或只给数据进程用。
查看 dmesg | grep HUGETLB,确认是否页不足/权限问题。
检查容器里 LD_PRELOAD 是否污染到 NCCL/驱动。
NUMA 绑核后吞吐更差
GPU PCIe root 与 CPU 亲和没绑对(用 nvidia-smi topo -m 看清楚)。
线程太少/太多都可能卡 pipeline,按 htop 观察负载再微调。
Docker 里 hugepages 不起效
忘了 --mount type=hugetlb 或页大小与主机不一致。
Pod/K8s 里没声明 hugepages 资源,容器拿不到。
tuned-adm 把 THP 改回 madvise
自定义 profile 固化:/etc/tuned/mytf/tuned.conf,transparent_hugepages=never。
开机后 cat 一眼确认,写健康检查脚本报警。
一键化脚本(可直接落地)
cat > tf_tune_rhel.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail
HP_2M=${HP_2M:-26000} # 可通过环境变量覆盖
NUMA_SPLIT=${NUMA_SPLIT:-1} # 1=按NUMA均分,0=全局
echo "[1/6] Disable THP (runtime)"
echo never | tee /sys/kernel/mm/transparent_hugepage/enabled
echo never | tee /sys/kernel/mm/transparent_hugepage/defrag
echo "[2/6] Persist THP=never (grubby)"
grubby --update-kernel ALL --args="transparent_hugepage=never" || true
echo "[3/6] Reserve 2MB HugeTLB: $HP_2M"
if [[ $NUMA_SPLIT -eq 1 ]] && [[ -d /sys/devices/system/node/node1 ]]; then
PERNODE=$((HP_2M/2))
echo $PERNODE | tee /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo $PERNODE | tee /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
else
echo $HP_2M | tee /proc/sys/vm/nr_hugepages
fi
mkdir -p /dev/hugepages && mountpoint -q /dev/hugepages || \
mount -t hugetlbfs nodev /dev/hugepages -o pagesize=2M,mode=1770
echo "[4/6] sysctl tuning"
cat >/etc/sysctl.d/90-tf-tuning.conf <<'EOF'
vm.max_map_count = 1048576
vm.swappiness = 1
vm.dirty_background_ratio = 5
vm.dirty_ratio = 15
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
vm.vfs_cache_pressure = 50
kernel.numa_balancing = 0
kernel.sched_migration_cost_ns = 5000000
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 268435456
net.ipv4.tcp_wmem = 4096 65536 268435456
EOF
sysctl --system
echo "[5/6] limits"
grep -q "nofile 1048576" /etc/security/limits.conf || {
echo "* soft nofile 1048576" >> /etc/security/limits.conf
echo "* hard nofile 1048576" >> /etc/security/limits.conf
}
echo "[6/6] verify"
grep -i huge /proc/meminfo | grep -E 'HugePages|Hugepagesize'
cat /sys/kernel/mm/transparent_hugepage/enabled
echo "Done. Reboot recommended to fully persist."
SH
sudo bash tf_tune_rhel.sh
训练侧的小技巧(把“水管”再掰直一点)
- tf.data:尽量 prefetch(tf.data.AUTOTUNE),num_parallel_calls=tf.data.AUTOTUNE;
- 大对象复用:避免频繁创建/销毁大张量;
- XLA:在 TF 2.15 上大多训练任务偏利好,但要测;
- NCCL:分布式下校准 NCCL_SOCKET_NTHREADS/NCCL_NSOCKS_PERTHREAD,跨机房时尤其重要;
- IO:远端 NAS/对象存储拉数据,充分用好缓存,必要时把热样本落本地 NVMe。
FAQ:2MB vs 1GB 大页我该选哪个?
先上 2MB:对大多数 TF 训练足够,兼容性好。
追求极稳与超大映射:再试 1GB,大幅减少 TLB miss,但预留压力更大、碎片化敏感。
混合也可:default_hugepagesz=1G hugepagesz=1G hugepages=16 + vm.nr_hugepages=26000,在不同映射上各取所长。
小结 & 验收清单
我当晚的“绿灯清单”:
- THP=never 开机后生效
- HugeTLB 2MB 预留到位,HugePages_Free 足够
- 关键进程用 hugectl --heap 启动并稳定运行
- NUMA 绑核与 GPU 拓扑一致
- page-faults、context-switches 明显下降
- images/sec 提升且波动收敛
结尾:从机房到天台
凌晨三点半,我把最后一条 nvidia-smi dmon 曲线收进日志,吞吐曲线像拉直的心电图。把机柜门轻轻关上时,手背蹭到金属边,有点冷。走到大楼天台,能看到海面反着微弱的灯火。
第二天客户的训练按时开跑,微信群里只飘过一句“今天很稳”。这种“没人发现你加班”的成就感,懂的人都懂。
如果你也正被 TensorFlow 的吞吐和抖动折腾,不妨按这套流程把 THP 关掉、HugeTLB 预留好,再把内核和 NUMA 拧紧。也许,你也会在某个深夜,看见那条终于乖顺的曲线。
附:常用检查速查
# THP 状态
cat /sys/kernel/mm/transparent_hugepage/{enabled,defrag}
# HugePages 余量
grep -E 'HugePages|Hugepagesize' /proc/meminfo
for n in /sys/devices/system/node/node*/hugepages/hugepages-2048kB/nr_hugepages; do echo $n: $(cat $n); done
# NUMA 拓扑/GPU 拓扑
numactl -H
nvidia-smi topo -m
# 进程是否命中大页(粗看)
grep -i huge /proc/$(pgrep -n python)/smaps 2>/dev/null | head
# 抖动观察
vmstat 1
iostat -x 1
perf stat -e page-faults,context-switches -p $(pgrep -n python) -- sleep 30