AI 推理服务的 Huge Pages 工程实战——从 THP 陷阱到 PMD 大页调优与 KSM 协同
Linux 内核的 Huge Pages 机制是高性能计算和 AI 推理服务中不可或缺的优化手段。然而,Transparent Huge Pages (THP) 在默认配置下的表现远非「银弹」——尤其在低延迟 AI 推理场景中,它往往是尾延迟毛刺的隐形元凶。本文将从 TLB Miss 的根源出发,深入剖析 THP 内部机制,并结合 AI 推理服务的内存访问模式,给出一套经过生产验证的调优方案。
一、为什么 AI 推理服务对 TLB Miss 极其敏感
现代 x86-64 CPU 的 L1/L2 TLB 通常可覆盖 64KB–2MB 的地址空间(4KB 页)。以一个 7B 参数 BF16 模型为例,推理时权重矩阵约 14GB,KV Cache 另需数 GB。若使用 4KB 页,仅权重就需要 367 万个页表项,远超 TLB 容量。
当 GPU 通过 PCIe BAR 映射主机内存进行推理时,每次 DMA 传输前的页面锁定(get_user_pages)若触发 Major Fault,延迟可飙升至数十毫秒。在 99th 百分位的在线推理服务中,这意味着单次请求延迟从 50ms 跳到 100ms+。
大页(2MB)将页表项数量压缩 512 倍,使 14GB 模型只需 ~7000 个 PMD 条目,显著提升 TLB 命中率。这是 Huge Pages 在 AI 推理中的核心价值所在。
二、Transparent Huge Pages 内部机制
2.1 khugepaged 与扫描算法
THP 的核心是内核线程 khugepaged,它周期性地扫描进程地址空间,尝试将符合条件的连续 4KB 页合并为 2MB 大页。关键参数如下:
# 扫描间隔(毫秒)
/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs # 默认 10000 (10s)
# 每次扫描的页数
/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan # 默认 4096
# 分配前等待的扫描轮数
/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none # 默认 512
khugepaged 的合并条件要求 512 个连续 4KB 页中有足够比例的页面已被访问。对于 AI 推理服务中「先分配、后均匀访问」的模式,这个条件往往很快满足。但问题在于——合并开销不可控。
2.2 四种 defrag 策略
always — 立即进行内存规整(可能阻塞页面分配)
defer — 推迟规整,先分配 4KB 页,失败时触发 direct compaction
defer+madvise — defer 基础上的 madvise 标记区域(默认推荐)
madvise — 仅对 MADV_HUGEPAGE 标记的区域启用 THP
在 AI 推理服务中,always 模式是灾难性的——它会在请求处理路径上触发 direct compaction,造成 10-100ms 级的延迟尖刺。madvise 模式则提供了精准控制的能力。
三、THP 在 AI 推理中的三大陷阱
3.1 尾延迟抖动
我们的生产环境曾出现 P99 延迟周期性从 60ms 跳到 180ms 的问题。通过 perf 采样发现,毛刺期间大量时间消耗在 khugepaged 的 collapse_huge_page 路径上:
# 捕获 THP 分配延迟
sudo perf trace -e hugetlbfs:hugetlbfs_alloc_inode \
-p $(pidof inference_server) --duration 5000
# 监控 THP fault 事件
sudo bpftrace -e '
kprob__do_huge_pmd_anon_fault {
@start[tid] = nsecs;
}
kretprob__do_huge_pmd_anon_fault /@start[tid]/ {
@lat_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
根因是 khugepaged 运行时需要临时映射大量物理页面、执行页复制和 TLB 刷新,这个过程在持有进程 mmap_lock 写锁时阻塞了新请求的内存访问。
3.2 内存膨胀与 OOM
THP 合并要求 2MB 物理连续内存。当系统运行数小时后,物理碎片化严重,khugepaged 会持续尝试 compaction,消耗 CPU 和内存带宽但不产生有效大页。更严重的是,被标记为 MADV_HUGEPAGE 但访问模式稀疏的内存区域(如模型加载器的临时缓冲区),会因 THP 的「预读」行为导致大量 2MB 实际被分配但仅使用一小部分,造成内存浪费。
3.3 与 GPU 显存映射的冲突
当使用 cudaHostRegister 或 cudaMallocHost 锁定主机内存供 GPU DMA 时,若内存被 THP 合并为 2MB 大页,CUDA 驱动需要确保整个 2MB 区域都被锁定。在内存压力下,内核可能无法锁住整个大页(page migration 失败),导致 CUDA 回退到 4KB 粒度的复制路径,性能骤降。
四、生产级调优方案
4.1 推荐配置:madvise + 精确标记
# 全局设为 madvice(或 never)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
在推理服务器代码中,仅对真正受益于大页的区域进行标记:
void* model_weights = mmap(NULL, model_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB,
-1, 0);
if (model_weights == MAP_FAILED) {
// 回退到 madvise
model_weights = mmap(NULL, model_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
madvise(model_weights, model_size, MADV_HUGEPAGE);
}
// 明确标记不需要 THP 的区域(如配置缓冲区)
void* config_buf = malloc(config_size);
madvise(config_buf, config_size, MADV_NOHUGEPAGE);
4.2 静态 Huge Pages(最高确定性)
对于延迟极度敏感的推理服务,完全禁用 THP,改用静态预分配 Huge Pages:
# 系统启动时预留 64GB 大页(假设 2MB/页 = 32768 页)
# 添加到 /etc/sysctl.conf 或 sysctl.d/
vm.nr_hugepages = 32768
vm.hugetlb_shm_group = 1002 # 推理服务的运行组
# NUMA 感知配置(双路服务器)
# /etc/sysctl.d/99-hugepages.conf
vm.nr_hugepages_mempolicy = "0:16384,1:16384"
应用程序通过 hugetlbfs 挂载点访问:
// 挂载点 /dev/hugepages 通常已预配置
#define HUGEPATH "/dev/hugepages/inference_weights"
int fd = open(HUGEPATH, O_CREAT | O_RDWR, 0660);
if (fd < 0) { /* 处理错误 */ }
void* addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_LOCKED | MAP_POPULATE,
fd, 0);
madvise(addr, size, MADV_WILLNEED); // 提前触发 page fault
4.3 针对 AI 推理的进阶优化
GPU Direct RDMA 场景: 使用静态 2MB hugetlbfs + ibv_reg_mr 注册内存,避免 THP 迁移导致的 MR dereg 开销。
struct ibv_mr* register_gpu_buffer(struct ibv_pd* pd, void* buf, size_t len) {
// buf 必须从 hugetlbfs mmap 获取
return ibv_reg_mr(pd, buf, len,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_RELAXED_ORDERING);
}
2GB 大页(PUD)用于超大模型: 对于 70B+ 参数模型(裸权重约 140GB),使用 1GB 大页可以进一步减少 TLB 压力。需在内核启用 CONFIG_TRANSPARENT_HUGEPAGE_PUD 并显式分配:
# 1GB 页需要 boot 参数
hugepagesz=1G hugepages=128 default_hugepagesz=1G
void* weights_1gb = mmap(NULL, model_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS |
MAP_HUGETLB | MAP_HUGE_1GB,
-1, 0);
五、KSM 与 THP 的协同设计
Kernel Same-page Merging (KSM) 与 THP 是一对需要谨慎搭配的组合。
5.1 冲突场景
KSM 扫描器会将内容相同的页面合并为一个写时复制(COW)页。但 KSM 以 4KB 粒度扫描,当一个 2MB 大页中的部分 4KB 页被 KSM 合并后,该大页无法再被 khugepaged 处理。反过来,THP 合并的 2MB 大页会跳过 KSM 扫描。
对于 AI 推理服务的多个副本进程(常见部署模式),KSM 可以合并重复的模型权重页面,节省物理内存,但会阻止 THP 大页形成。
5.2 推荐策略:分区处理
# 对推理服务:禁用 KSM(权重通常不跨进程共享,或已通过 mmap 共享)
echo 0 > /sys/kernel/mm/ksm/run
# 对 Redis/缓存类共享数据的容器环境:启用 KSM
echo 1 > /sys/kernel/mm/ksm/run
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan
echo 10 > /sys/kernel/mm/ksm/sleep_millisecs
混合调度方案: 若必须同时使用 KSM 和 THP,通过 cgroup v2 对不同工作负载分别控制:
# 推理 Pod:THP + no KSM
mkdir /sys/fs/cgroup/inference
echo madvise > /sys/fs/cgroup/inference/memory.thp.enabled
# Redis Pod:KSM + no THP
mkdir /sys/fs/cgroup/redis
echo never > /sys/fs/cgroup/redis/memory.thp.enabled
echo 1 > /sys/fs/cgroup/redis/memory.ksm.run # 通过 cgroup v2 KSM 接口
(注:cgroup v2 的 per-cgroup THP 控制已在 kernel 5.15+ 支持,KSM 的 per-cgroup 控制仍在演进中)
六、监控与性能评估
6.1 关键指标
# 当前 THP 使用状态
cat /proc/meminfo | grep -i huge
# AnonHugePages: 3276800 kB ← 实际使用中的 THP 匿名映射
# HugePages_Free: 16384
# HugePages_Total: 32768
# 进程级别的 THP 使用情况
cat /proc/$(pidof inference_server)/smaps | grep -B1 AnonHugePages
# TLB Miss 采样(Intel)
sudo perf stat -e dTLB-load-misses,dTLB-store-misses \
-p $(pidof inference_server) -I 1000
6.2 BPF 追踪 THP 分配延迟
// trace_thp.bpf.c
SEC("kprobe/do_huge_pmd_anon_fault")
int trace_thp_fault(struct pt_regs *ctx) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 tid = pid_tgid;
u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start_map, &tid, &ts, BPF_ANY);
return 0;
}
SEC("kretprobe/do_huge_pmd_anon_fault")
int trace_thp_fault_ret(struct pt_regs *ctx) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 tid = pid_tgid;
u64 *tsp = bpf_map_lookup_elem(&start_map, &tid);
if (tsp) {
u64 delta_us = (bpf_ktime_get_ns() - *tsp) / 1000;
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &delta_us, sizeof(delta_us));
}
return 0;
}
七、AI 推理场景决策树
是否需要 <100μs 的确定性延迟?
├─ 是 → 静态 Huge Pages (hugetlbfs) + 禁用 THP + 禁用 KSM
│ └─ 启动时预分配 + 进程锁内存 (MRLOCK)
│
└─ 否 → 工作集是否可被 2MB 对齐访问?
├─ 是 → madvise 模式 + 精准标记热点区域
│ └─ khugepaged 扫描间隔设为 60s(减少干扰)
│
└─ 否(稀疏访问)→ never 模式 + 依赖硬件 IOMMU TLB
└─ 考虑 1GB 大页 (Pud) 减少页表深度
八、最佳实践清单
- 永远不要在生产 AI 推理服务上使用
alwaysTHP 模式——defrag会在请求处理路径触发不可预测的 compaction。
- 启用
madvise+ 代码显式标记:用MADV_HUGEPAGE标记模型权重和 KV Cache 区域,用MADV_NOHUGEPAGE排除配置缓冲、日志缓冲等低频区域。
- 超高延迟敏感场景使用 hugetlbfs 静态大页:启动时
MAP_POPULATE | MAP_LOCKED一次性完成所有 page fault,运行时零抖动。
- THP 与 KSM 二选一:除非你有非常明确的分区方案,否则不要在同一个进程空间中混合使用。
- GPU Direct 场景必须使用大页:
cudaHostRegister在 4KB 页上的锁定开销巨大,2MB 大页可使 GPU DMA 注册延迟降低 10-50 倍。
- 监控
AnonHugePages占比:若占比远小于工作集大小,说明 THP 合并失败,需检查碎片化或改用 hugetlbfs。
- NUMA 感知分配:绑定 NUMA 节点的大页确保 GPU-CPU 内存访问走本地节点,避免跨节点带宽瓶颈。
结语: Huge Pages 调优不是简单的 sysctl 开关,而是需要理解 AI 推理工作负载的内存访问模式,在"确定性延迟"与"自动化管理"之间做出权衡。当你的推理服务从原型走到生产,THP 的默认行为往往是第一个需要被推倒和重新设计的子系统。

发表评论 取消回复