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

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

发布人:Minchunlin 发布时间:2025-09-08 10:51 阅读量:630


那天晚上我一个人在香港机房值班,机柜第 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
目录结构
全文