刚接手一个推理服务性能优化时我被一个现象卡了整整三天CPU 使用率已经冲到 95% 以上GPU 利用率却只有 40% 左右QPS 死活上不去。perf 一抓发现系统把大量时间花在了内存地址翻译上dTLB-load-misses 每分钟几千万次。我一度以为是算子实现的问题把 attention、GEMM 换了好几版都没用。最后真正解决问题的是一个几乎没人提起的参数——Linux 内存页大小把默认的 4K 页面换成 2M 甚至 1G 的大页P99 延迟直接降了将近三成。这就是标题里说的隐藏旋钮页大小。它不像 GPU 算子、通信库、显存分配器那样天天被人讨论但它决定了 CPU 在访问内存时TLB页表缓存能不能覆盖住你真正的数据工作集。对大模型推理、训练这类动辄几十 GB 权重、数 GB KV Cache 的负载来说4K 默认页面会让 CPU 在地址翻译上浪费大量周期。这篇文章不打算从操作系统教科书讲起而是从一个 AI Infra 工程师的实际视角把页大小的原理、影响路径、配置方式、排查链路一次讲清楚。适合做推理服务、训练平台、GPU 虚拟化以及所有和 CPU 内存访问打交道的同学参考。1. 隐藏的根源虚拟内存地址翻译与 TLB 命中率决定了 CPU 访存的真实成本1.1 CPU 的翻译官一次内存访问背后到底发生了什么现代操作系统里应用程序看到的地址是虚拟地址不是物理地址。CPU 拿到一个虚拟地址后必须把它翻译成物理地址才能去内存里取数据。这个翻译过程依赖页表——一张记录了虚拟页到物理页映射关系的多级表格。问题在于页表本身也存在内存里。如果每次访问数据都要顺着多级页表一层层查下去那访问一次内存等于访问好几次内存性能直接崩盘。所以 CPU 硬件里加了一层缓存叫 TLB专门缓存最近用过的虚拟地址到物理地址的翻译结果。TLB 命中的话翻译开销微乎其微TLB 未命中CPU 就要去做一次完整的页表遍历在 x86 上通常要访问四级页表耗费几十甚至上百个 CPU 周期。这个机制本身不复杂但它带来一个直接影响TLB 能覆盖的内存大小取决于 TLB 条目数和页大小。1.2 TLB 容量小得离谱4K 页面对大模型权重有多不友好桌面 CPU 的 L1 dTLB 通常只有 64 个条目左右L2 TLB 也就 1500 个条目上下。这个数量听着不少但算一笔账就明白了默认 4K 页面64 个 TLB 条目只能覆盖 64 × 4K 256KB 的地址空间2M 大页64 个条目能覆盖 64 × 2M 128MB1G 大页64 个条目能覆盖 64 × 1G 64GB一个 7B 参数的 FP16 模型光权重就是 14GB。按 4K 页面算需要 350 万个页面才能映射完按 2M 页面算只需要 7168 个按 1G 页面算仅 14 个。TLB 条目是有限的页面越小它记住的地图就越碎片化覆盖的总面积就越小。当进程顺序扫描这 14GB 权重时4K 页面下 TLB 会不断被新地址冲掉反复 miss2M 或 1G 页面下TLB 能长时间覆盖住整块权重区域后续访问几乎全部命中。这就是为什么大模型推理、训练这类大工作集负载对页大小极度敏感。1.3 为什么 AI 场景会反复触发 TLB Miss大工作集与流式访问AI 负载的访存特征和传统 Web 服务完全不同。Web 服务的工作集通常很小几十 KB 的热数据可能一直留在 TLB 里AI 负载则是动辄几十 GB 的大工作集而且访问模式高度流式前向计算要反复读取全部权重注意力计算要扫描不同序列位置的 KV Cache训练还要读写优化器状态和梯度。流式访问对 TLB 最不友好——你的 TLB 刚缓存了前面的映射马上又被后面的新地址淘汰了几乎没有局部性可以利用。这类负载如果不加大页CPU 会陷入每次访存都在翻译地址的泥潭表现出来就是 CPU 忙得不行但真正的计算、访存效率很低。我见过不少团队把推理性能差的锅甩给算子、显存碎片或者框架选型最后 perf 一查dTLB-load-misses 占了很大比例换成大页后立竿见影。页大小确实是个不起眼的底层参数但它在 AI 负载下的放大器效应远比很多人想象的大。2. 4K、2M、1G 三种页面的本质差别不只是数字翻倍而是页表层级和系统行为的差异2.1 页表层级2M 跳过一级1G 跳过两级x86_64 的默认四级页表结构是 PML4 → PDPT → PD → PT → 物理页。每一级都是一次内存访问。如果采用 4K 页面地址翻译要走完全部四级如果采用 2M 大页PT 这一级可以跳过直接由 PD 项映射到物理地址采用 1G 大页PT 和 PD 都能跳过由 PDPT 直接映射。页表层级减少带来的收益是双重的一是单次 page walk 的内存访问次数变少二是 TLB 条目能覆盖的物理内存范围大幅扩大。前者在 TLB miss 已发生时降低损失后者直接减少 miss 发生的概率。这两个效果叠加就是大页在 AI 负载上收益惊人的根本原因。另外还有一层容易被忽略的影响页表本身占据物理内存。4K 页面下14GB 权重就需要几万个页表项页表本身也要占用几十 MB 内存大页可以把页表开销降一个数量级甚至两个数量级。在物理内存紧张、页表频繁换入换出的系统里这部分差别也会传导到性能上。2.2 三种页面适用场景对照我整理了一个简单的对照表可以直观看到三种页面的差异页面大小覆盖 14GB 权重所需映射数TLB 单条目覆盖范围典型适用场景主要代价4K约 350 万4KB通用默认、小内存负载大工作集下 TLB miss 严重2M约 71682MB推理服务、训练进程、一般大页需求内部碎片、需要显式配置或 THP1G141GB超大权重加载、高性能计算、数据库大缓冲必须预留、分配失败影响大实际项目里2M 大页是最常用的折中方案因为它能通过 THP透明大页机制自动完成大部分工作应用改动很小。1G 大页适合那种内存需求确定、启动期一次性分配完整块内存的场景比如推理引擎启动时加载全部权重、KV Cache 预留等。2.3 大页的代价碎片、预留、延迟与兼容性大页不是免费的它有几个绕不开的代价。第一是内部碎片。大页的最小分配单位是 2M 或 1G如果应用只需要几百 KB也必须占一个完整的 2M 页剩余空间就浪费了。对内存需求量小且碎片化的进程开大页反而可能让内存占用大幅上涨。第二是预留问题。HugeTLBfs 方式的大页需要提前从系统内存中锁出来锁出来的内存不能给其他进程用。如果预留太多系统可用内存变少预留太少应用启动时可能分配失败。第三是延迟抖动。THP 的 always 模式有自动内存规整的逻辑内核为了合并大页会触发内存 compaction在内存碎片较多时可能产生明显的延迟毛刺。对推理服务的 P99 来说这种毛刺很致命。所以大页不是越大越好也不是不分场景直接开。正确做法是理解每种页面在系统层面的行为差异再结合负载特征选型。3. 从模型推理到训练通信页大小在 AI 负载里的真实影响路径3.1 推理服务KV Cache 与权重扫描的访问形态推理服务的访存大头有两块权重扫描和 KV Cache。权重扫描是典型的顺序流式读取。每一次请求都要读一遍全部激活层权重不同请求之间几乎没有地址局部性。这种情况下TLB 实际上帮不上什么忙关键是减少每次 miss 的开销。用 2M 大页之后同样的地址空间只需要 4K 页面 1/512 的页表项数量page walk 少走一级性能收益非常直接。KV Cache 则复杂一些。它是动态分配的会随着请求数量、上下文长度增长而不断改变大小访问模式是密集但随机性较强的读取。KV Cache 分配到非大页内存时每次访问任意位置都可能触发 TLB miss如果提前预留了充足的大页池KV Cache 可以落在大页上随机访问的代价就能显著下降。市面上主流推理引擎的 KV Cache 都有独立的分配池这里恰恰是页大小发挥作用的主战场。3.2 训练集群优化器状态、梯度 AllReduce 与 pinned memory训练侧的情况和推理不完全一样但同样存在页大小导致的开销。训练进程的内存占用通常比推理更大尤其是 Adam 等优化器需要保存一阶、二阶动量参数每增一个单位优化器状态就要增加两个单位。一个 70B 的模型仅优化器状态就可能达到数百 GB。这部分内存的访问频率极高如果以 4K 页面铺设TLB miss 的代价会被充分放大。梯度 AllReduce 也有影响。通信库在梯度同步前通常要求把梯度数据整理到连续的内存缓冲区里再交给 NCCL 或 MPI 处理。如果这些缓冲区的虚拟地址映射分散在大量 4K 页面里通信库的零拷贝路径可能会退化DMA 描述符数量暴涨传输效率下降。换用大页后缓冲区映射连续DMA 和内核的处理开销都会明显降低。还有一种容易被忽略的场景是 DataLoader 的 pin_memory。PyTorch 的 pin_memoryTrue 会把 CPU 侧数据放到 page-locked 内存里用于加速 CPU 到 GPU 的拷贝。但 page-locked 并不等于大页它锁定的还是 4K 页面。在数据预处理吞吐极高的训练任务里这批 pinned buffer 同样会产生大量 TLB miss只是它被 GPU 拷贝的耗时掩盖住了。3.3 CUDA 与 PyTorch 生态里页大小是怎么被藏起来的在 CUDA 生态里页大小并没有完全消失只是被藏得更深了。cudaMallocHost / cudaHostRegister 分配的虽然都是 page-locked 内存但如果底层页面是 4KCPU 侧访问时 TLB 压力依然在。cudaHostRegister 还有一个隐含成本把一段普通内存注册成 pinned 时内核需要对涉及的页面做 TLB shootdown页面数量越多这个操作越贵。GPU 显存侧也存在页大小的概念。新版本 CUDA 的 VMM API 允许按更粗的粒度映射显存部分推理引擎已经利用这个能力把 GPU 侧的页表开销降下来比如 vLLM 新版本提供的 HugePages 支持以及 TensorRT-LLM 的 pinned buffer 配置。这类优化本质上和 CPU 侧的大页是同一套思路只是作用对象从系统内存换成了显存。3.4 先澄清三个容易混淆的4K画质、存储对齐与内存页网上搜4K出来的大概率是视频分辨率这跟内存页完全是两回事。我见过不止一个同学把内存页大小调成 4K和视频开不了 4K 画质搞混这里有必要做个明确区分。内存页的 4K指的是 4096 字节4KiB是操作系统管理虚拟内存的最小映射单位。视频的 4K指的是 3840 × 2160 像素的分辨率跟内存一点关系都没有。抖音电脑版开不了 4K 画质通常是解码能力、网络带宽或显示设备不支持找内存页配置属于南辕北辙。存储领域的4K 对齐是另一个容易混淆的概念。SSD 物理扇区是 4096 字节分区和文件系统如果没对齐到 4096 字节的整数倍读写就会出现跨扇区的额外 IO。这是存储设备层的对齐问题和内存分页机制无关。图片处理里的4K 超清更是纯粹的画质增强话题。把这些概念分开能避免在实际排查时走弯路。内存页大小解决的问题是 CPU 访存效率这个定位要先搞清楚。4. 把隐藏旋钮变成显式参数大页配置、框架接入与收益验证4.1 三种开启大页的方式HugeTLBfs、THP、libhugetlbfsLinux 下开启大页的路径主要有三条适用场景各有不同。第一是 HugeTLBfs最传统的方式。系统启动后通过 sysctl 或 /proc 接口预留大页挂载一个 hugetlbfs 文件系统应用通过 mmap 映射其中的文件来使用大页。它的优势是支持 1G 页面且内存分配完全可控劣势是需要预留且应用要显式适配。# 预留 64 个 2M 大页 sysctl -w vm.nr_hugepages64 # 预留 8 个 1G 大页按 NUMA 节点分配 echo 8 /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages # 挂载 hugetlbfs mkdir -p /mnt/hugepages mount -t hugetlbfs -o pagesize2M none /mnt/hugepages第二是 THP透明大页。它不需要预先预留内核在分配内存时尝试自动使用 2M 大页应用无感。THP 有 always、madvise、never 三种模式。always 模式对所有内存分配都尝试大页可能出现我前面说的延迟毛刺madvise 模式只在应用显式调用madvise(MADV_HUGEPAGE)的内存区域使用大页可控性好得多。# 查看当前模式 cat /sys/kernel/mm/transparent_hugepage/enabled # 推荐 madvise 模式 echo madvise /sys/kernel/mm/transparent_hugepage/enabled # defrag 控制内存规整强度延迟敏感服务建议关闭或设为 defer echo defer /sys/kernel/mm/transparent_hugepage/defrag第三是 libhugetlbfs。它通过 LD_PRELOAD 截获应用的内存分配调用把大块分配自动转发到大页上适合没法改代码的老程序。不过现代 AI 框架基本都是 Java、Python、C 混跑LD_PRELOAD 方式容易误伤我一般只把它当兜底方案。4.2 框架侧接入CUDA pinned memory、vLLM、PyTorch DataLoader配置好系统大页之后还要让 AI 框架真正用上这一步经常被忽略。PyTorch 的 DataLoader 里pin_memoryTrue会调用 cudaHostAlloc 分配 pinned memory但这块内存默认仍然是 4K 页面。如果你已经给系统提前预留了 HugeTLBfs 大页可以考虑在数据加载进程里使用madvise(MADV_HUGEPAGE)标记 pinned buffer 区域让 THP 在分配时做大页处理。更简单的方式是检查/proc/pid/smaps里的 AnonHugePages 字段确认数据加载进程是否真正拿到了大页。vLLM 新版本提供了基于 CUDA VMM API 的大页分配能力通过环境变量控制 KV Cache 的映射粒度。开启后 GPU 侧的页表开销会下降长上下文场景的 prefill 延迟有可感知的改善。TensorRT-LLM 则通过 pinned buffer 配置项控制 CPU 侧缓存通常建议把这部分 buffer 的页面大小也纳入大页管理。框架和系统层面都配好以后还要注意 NUMA 亲和性。1G 大页在 NUMA 架构下如果分配在不正确的节点上跨节点访问延迟会抵消掉大页带来的收益。建议结合 numactl 把推理进程绑定到物理内存所在的 NUMA 节点给 1G 大页设置numactl --interleaveall时也要先确认工作负载对跨节点访问的容忍度。4.3 用 perf stat 与 /proc 指标量化收益配置做完了一定要量化验证不能只凭感觉。我会在优化前后各跑一轮压测记录以下指标。perf 是最直接的观测工具# 统计进程的 TLB 事件 perf stat -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses -p PID sleep 10重点关注 miss 的比例。如果优化前 dTLB-load-misses 占总访问量的 5% 以上优化后降到 1% 以下那页大小调整的效果已经非常明显了。/proc 下的几个文件也很有用# 查看系统大页使用情况 grep -i hugepages /proc/meminfo # 查看进程的匿名大页占用 grep AnonHugePages /proc/pid/smaps # 查看进程页表占用VmPTE 越小说明页表开销越低 grep VmPTE /proc/pid/status我曾在一台双路服务器上做过对比同样一个 7B 模型推理服务4K 页面下 dTLB-load-misses 约 3.2%P99 延迟 42ms切换到 2M 大页后dTLB-load-misses 降到 0.4%P99 降到 33ms吞吐提升约 18%。不是每个场景都有这么夸张的收益但大页在 AI 负载上的影响力绝对不能低估。4.4 几个容易踩的坑配置过程中有几个坑值得单独提醒。HugeTLBfs 的大页是预留给特定用途的系统不会自动回收。如果预留过多其他进程的内存申请会失败预留过少启动时大页分配失败又会让应用直接崩溃。我建议先跑一次压测观察峰值大页使用量再留 10%-20% 余量。THP 的 always 模式在内存碎片较多的老系统上可能触发频繁的 direct compaction造成延迟尖刺。对这种系统我建议改成 madvise 模式只对明确标记的区域使用大页其余内存保持 4K 页面。用 THP 的 madvise 模式时还要确认应用确实调用了madvise(MADV_HUGEPAGE)比如 JVM 要加-XX:UseTransparentHugePages、Python 的某些内存分配器不一定会转发这个调用。还有一个容易忽略的点容器场景下的页大小。容器里的 /proc 是隔离的但内核参数还是宿主机的。你在容器里改 /sys/kernel/mm/transparent_hugepage/enabled 可能没权限或没效果需要宿主机层统一配置。另外Kubernetes 对 HugePages 有专门的资源字段需要提前在节点上预留并设置对应 limit否则 Pod 根本申请不到大页。5. 一次 dTLB Miss 现场排障从吞吐上不去到确认页问题5.1 现象与第一轮排查CPU 高、IO 低、GPU 不饱和那次排障的对象是一个基于自研 C 推理引擎的服务部署在 8 卡 A100 的节点上压测时表现非常典型CPU 全部核心几乎打满GPU 利用率却只有 40%吞吐远低于预期P99 抖动明显。第一轮排查走的是常规路线查 CPU 热点函数看到大量时间花在memcpy和memset上查 IO几乎没有磁盘和网络瓶颈查锁竞争没有明显争抢。这一轮下来问题不仅没定位反而更扑朔迷离了——内存拷贝本身就是正常操作为什么拷贝会慢到拖垮整个服务5.2 定位perf 与 /proc/ /smaps 的关键证据第二轮我决定不猜了直接用 perf stat 看内存子系统的事件。结果出来后dTLB-load-misses 占比高得离谱达到了 6% 以上dTLB-store-misses 也不低。这意味着每 16 次内存读取就有一次需要走完整页表遍历代价极其昂贵。接着看/proc/pid/smaps发现进程的 AnonHugePages 字段基本没有增长说明 THP 几乎没有生效。原因很快找到了服务启用了自己的内存池分配内存用的是mmap但没有显式调用madvise(MADV_HUGEPAGE)THP 的 madvise 模式下根本不会主动给它用大页。它自己的内存池又是按块动态增长的在 4K 页面下页表蔓延得非常夸张VmPTE 显示页表本身占了接近 200MB 内存。5.3 修复madvise 显式大页 重启验证修复方案分两层。第一层把服务的内存池分配逻辑加上madvise(MADV_HUGEPAGE)让 THP 在 madvise 模式下也能对它生效第二层单独给权重加载和 KV Cache 预留了 HugeTLBfs 的 2M 大页池这两个模块的内存需求在启动期就是确定的用显式大页最可控。重启服务后再看dTLB-load-misses 的比例从 6% 降到了 0.8% 左右smaps里的 AnonHugePages 明显增长VmPTE 从 200MB 降到 40MB 不到。压测数据也印证了前面的判断P99 延迟下降约 28%吞吐提升了约 15%。这个过程让我印象最深的一点是问题不是出现在页大小配置有没有开这个层面而是出现在应用自己的内存分配路径有没有接上大页这个层面。系统层面开着 THP应用却绕过了 THP 的生效条件等于白开。5.4 复盘什么时候值得调页大小结合这次排障和后来做过的多个项目我对什么时候值得调页大小有了比较明确的判断标准。如果服务的内存工作集超过 100MB且访问模式偏流式或大块连续扫描那页大小调整的收益通常很明显值得优先尝试。如果工作集只有几十 MBTLB 本身就能覆盖住大部分访问调页大小意义不大。如果服务主要在 GPU 侧跑大规模算子CPU 侧只是调度和分发那页大小的影响也会被 GPU 计算掩盖住。还有一个前置条件先确认 CPU 侧确实是瓶颈。如果 GPU 已经打满、CPU 大部分时间在等待那页大小怎么调都白搭。只有 CPU 成为瓶颈时TLB miss 才会从次要矛盾变成主要矛盾。最后再分享两个我自己反复验证过的小原则一是永远先量后调。在优化页大小前先用 perf stat 确认 dTLB-load-misses 的占比用事实驱动决策而不是看到大页就无脑上。调完以后再用同一套指标验证让收益可量化、可回归。二是大页要分模块对待。一个 AI 服务里权重加载区、KV Cache 区、动态中间结果区、数据加载区对页大小的需求完全不同。启动期确定的大块内存用 HugeTLBfs 显式预留动态增长的中间数据用 THP madvise 标记零散的临时小对象保持 4K 页面。这种分而治之的做法比笼统地把整台机器切成大页模式要稳妥得多。页大小这个旋钮平时没人注意但它在 AI 负载下的表现确实对得起隐藏旋钮这个说法。希望这篇内容能帮你在下一次 CPU 打满但吞吐上不去的时候少走几步弯路。
