从 x86-64 四级页表到 ARM64 五级页表:Linux 内核页表遍历在 AI 推理大内存场景中的深度工程
现代 AI 推理引擎的工作集正在经历爆炸式增长。以 Llama-3 70B 模型为例,仅模型权重就占据 140GB 显存,加上 KV Cache、激活值和中间张量,单台推理服务器的内存占用轻松突破 200GB。当内存从"充足"变成"珍贵",一个曾经隐藏在底层抽象之下的子系统走到了前台——页表遍历(Page Table Walk)。
本文将深入剖析 Linux 内核中从 x86-64 的四级页表到 ARM64 五级页表的完整架构,重点关注 AI 推理场景中的实际问题:为什么 57 位虚拟地址空间对你的推理引擎至关重要?硬件 TLB 的 misses 如何拖累 throughput?Huge Pages 真正解决了什么?而当硬件无法提供足够大的 TLB 覆盖时,我们又该如何在软件层面做补偿?
一、为什么 AI 工程师应该关注页表遍历
传统后端开发者很少需要关注虚拟内存子系统。Page Fault 发生,内核处理它,一个 page 被映射,进程继续运行。这套机制在大多数场景下工作得很好,以至于我们几乎忘记了它的存在。
但在 AI 推理中并非如此。推理引擎有几个与众不同的内存访问模式:
- 巨大且连续的内存块:模型权重通常以 50GB-200GB 的连续映射加载到内存中
- 随机访问模式:Attention 计算的 softmax 和加权求和导致对 KV Cache 的大量随机读取
- 高频 small writes:推理过程中生成 token 时的 KV Cache 更新带来大量小规模随机写入
- 长时间运行:推理服务不像训练任务那样有明确的结束点,内存碎片化会持续累积
这些模式让 TLB(Translation Lookaside Buffer)的命中率成为性能瓶颈。一次 TLB miss 意味着额外的页表遍历——在 x86-64 四级页表中最多 4 次额外内存访问,在 ARM64 五级页表中最多 5 次。如果一次内存读取本身需要 100ns,在最坏情况下一次 TLB miss 可以将单次内存访问延迟推高到 600ns 甚至 800ns。
在实际推理 benchmark 中,200GB 工作集配合默认 4KB 页会导致 TLB 覆盖严重不足。通过 perf stat -e dTLB-load-misses,iTLB-load-misses 我们能看到,在长上下文推理场景中,TLB miss 率可以达到 15%-25%,严重影响吞吐量。
二、x86-64 页表架构:从四级到五级
2.1 四级页表及其地址空间限制
x86-64 的标准页表实现使用 4 级页表结构,支持 48 位虚拟地址(256TB):
CR3 → PML4 → PDPT → PD → PT → Physical Page
0-47 47-39 38-30 29-21 20-12 12-0
每一级使用 9 位地址索引(512 个条目 × 8 字节 = 4KB,恰好占一个 page)。CR3 寄存器指向 PML4 表的基地址,MMU 在硬件层面遍历这张表完成虚拟地址到物理地址的翻译。
48 位虚拟地址空间分成两段:
0x0000_0000_0000_0000-0x0000_7FFF_FFFF_FFFF:用户空间(128TB)0xFFFF_8000_0000_0000-0xFFFF_FFFF_FFFF_FFFF:内核空间(128TB)
这个 256TB 的地址空间在十年前绰绰有余。但对于今天的 AI 工作负载,128TB 的用户空间在面对多模型并行推理、大规模 KV Cache 和不断增长的上下文窗口时开始显得紧张。
2.2 Intel 5-Level Paging:57 位虚拟地址
Intel 处理器自 Ice Lake(部分)和 Tiger Lake 开始、AMD 自 Zen 4 架构开始支持 5 级页表(PML5),将虚拟地址空间扩展到 57 位(128PB):
PML5 → PML4 → PDPT → PD → PT → Physical Page
56-48 47-39 38-30 29-21 20-12 12-0
多出一级 PML5 意味着:
- 虚拟地址从 48 位扩展到 57 位(128PB)
- 每次 TLB miss 时页表遍历多一次内存访问(5 次而非 4 次)
- 新地址空间布局:用户空间 64PB,内核空间 64PB
启用方式通过设置 CR4 寄存器的 LA57 位。Linux 内核在启动时通过检测 CPU 能力决定是否启用:
// arch/x86/include/asm/cpufeature.h
#define X86_FEATURE_LA57 (12*32+16) /* 5-level paging */
// arch/x86/mm/init.c
static int __init parse_clearcpuid(char *arg)
{
...
if (!strcmp(arg, "la57"))
disable_feature(X86_FEATURE_LA57);
...
}
在实际部署中,5 级页表的价格是 TLB miss 延迟增加约 20-25%。但对于需要超大地址空间的 AI 推理引擎,这种 trade-off 是值得的——它让单个进程可以映射多达 64PB 的虚拟地址空间。
2.3 Linux 内核中的页表遍历代码
Linux 内核提供了一套与架构无关的页表遍历 API。核心数据结构是 pgd_t → p4d_t → pud_t → pmd_t → pte_t,对于四级页表则折叠掉 p4d 级:
// include/linux/pgtable.h
static inline pgd_t *pgd_offset(struct mm_struct *mm, unsigned long addr)
{
return mm->pgd + pgd_index(addr);
}
static inline p4d_t *p4d_offset(pgd_t *pgd, unsigned long addr)
{
return (p4d_t *)pgd + p4d_index(addr);
}
static inline pud_t *pud_offset(p4d_t *p4d, unsigned long addr)
{
return (p4d_t *)p4d + pud_index(addr);
}
// 宏折叠:当只有 4 级时,p4d 直接映射为 pgd 级别
#define p4d_offset(pgd, address) ((p4d_t *)pgd)
这种设计让同一份内核代码在四级页表和五级页表模式间无缝切换。对于 AI 推理引擎而言,这意味着同一个二进制可以在 48 位和 57 位地址空间上运行,无需重新编译。
三、ARM64 页表架构:更复杂的设计空间
3.1 ARM64 页表的基础布局
ARM64 的页表设计比 x86-64 更加灵活,支持多种页大小(4KB、16KB、64KB)和不同的地址宽度。
在 4KB granule 配置下,标准 4 级页表如下:
TTBR0/TTBR1 → L0 → L1 → L2 → L3 → Physical Page
47-39 38-30 29-21 20-12 11-0
其中 TTBR0 用于用户空间(低地址),TTBR1 用于内核空间(高地址),这种分离允许用户/内核切换时无需刷新 TLB。
ARM64 的独特之处在于每一级的条目格式更加丰富。除了基本的 VALID、TABLE、BLOCK 标志位,ARM64 还提供了:
- Access Flag (AF):硬件自动置位,用于页面回收策略
- DBM (Dirty Bit Modifier):硬件脏页跟踪
- Contiguous Bit:将 16 个连续的 PTE 合并为一个 TLB 条目,等效提供大页的效果
- [PXN, UXN]:特权/非特权执行权限(对 AI 推理引擎中代码注入防护至关重要)
3.2 ARM64 52-bit VA:LPA2 与 5 级页表
ARM64 在 ARMv8.2 引入了 LPA(Large Physical Address)支持,通过修改页表描述符格式将物理地址扩展到 48-52 位。在 ARMv8.7+ 和 Cortex-X2/A710/A715 等核心上,进一步支持了 52 位虚拟地址:
L0 → L1 → L2 → L3 → L4 → Physical Page
51-42 41-33 32-24 23-14 13-12 11-0
但与 x86 的 5 级页表不同,ARM64 的 52 位实现有两种路径:
LPA2(ARMv8.2+):保留 4 级页表结构,通过扩展每个 PTE 中的物理地址位数实现。需要 OS 支持:
// arch/arm64/include/asm/pgtable-hwdef.h
#define ARM64_HW_PGTABLE_LEVELS(((VA_BITS - PGDIR_SHIFT) / PGDIR_SHIFT) + 1)
// 当 LPA2 启用时,每个 PTE 中 OSA 字段扩展到 52 位
#define PTE_ADDR_MASK (((1UL << 52) - 1) & PTE_ADDR_LOW)
5 Level Paging(ARMv8.7+):类似 x86 的方案,直接增加一级页表:
// arch/arm64/include/asm/mmu_context.h
static int __init map_entry_trampoline(void)
{
pgprot_t prot = ...;
if (pgtable_l5_enabled())
// 使用页表的第 5 级处理 52 位地址
...
}
3.3 Contiguous PTE:ARM64 的秘密武器
ARM64 的 Contiguous Bit(bit [52] in PTE)是一个极具工程价值的特性。它允许 16 个连续的 PTE 被硬件合并为一个 TLB 条目:
// arch/arm64/include/asm/pgtable-hwdef.h
#define PTE_CONT (_AT(pteval_t, 1) << 52)
#define PTE_CONT_COUNT 16 // 16 个连续 PTE 合并
// 设置 Contiguous bit 的核心逻辑
static inline pte_t pte_mkcont(pte_t pte)
{
pte = pte_set_bit(pte, __pgprot(PTE_CONT));
return pte;
}
这意味着对于一个 16 × 4KB = 64KB 的连续映射区域,只需要 1 个 TLB 条目而非 16 个。在 AI 推理引擎中,KV Cache 的连续内存块非常适合使用 Contiguous PTE 来优化。Grace Hopper (ARM64 + Hopper GPU) 架构中,Contiguous PTE 配合 GPU 共享内存的 unified memory 机制,可以将 TLB miss 率降低 40% 以上。
四、内核视角:页表遍历与 AI 推理的交互
4.1 缺页异常路径分析
AI 推理引擎中的缺页异常发生在两个主要场景:
Lazy Allocation Fault(懒分配缺页):当推理引擎通过 mmap 预留大量虚拟地址空间但未实际访问时触发。例如:
// 推理引擎常见模式:预映射大量地址空间
void *kv_cache_base = mmap(NULL, 100ULL * 1024 * 1024 * 1024,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
-1, 0);
// 实际按需填充 KV Cache
Linux 内核的 handle_mm_fault 处理这种懒分配:
// mm/memory.c
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
...
if (!vmf->pte) {
// 完全未映射:分配新的 PTE 和物理页
return do_anonymous_page(vmf);
}
if (!pte_present(*vmf->pte)) {
// 页面被 swap 或未分配:需要重新映射
return do_swap_page(vmf);
}
...
}
Prefault 优化:对于 AI 推理的确定性内存访问模式,内核提供 MAP_POPULATE 和 madvise(MADV_POPULATE_READ/WRITE) 来预填页表,避免运行时缺页的延迟尖刺:
// 推荐:AI 推理引擎的 KV Cache 初始化模式
void *kv_cache = mmap(NULL, cache_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,
-1, 0);
// 或者使用 madvise 做更细粒度的控制
madvise(kv_cache, cache_size, MADV_POPULATE_WRITE);
4.2 Transparent Huge Pages vs Explicit Huge Pages
AI 推理引擎通常使用显式 2MB Huge Pages 来映射模型权重区域,而让 KV Cache 使用默认 4KB 页加 THP 自动提升。
/sys/kernel/mm/transparent_hugepage/enabled 的两种策略对比:
always: 整个进程地址空间尝试使用 THP
madvise: 仅在 madvise(addr, len, MADV_HUGEPAGE) 区域启用 THP
对于 AI 推理引擎,推荐使用 always 模式(配合足够的 /proc/sys/vm/nr_hugepages),原因如下:
- 模型权重区:使用显式 2MB Huge Pages 是最优选择,TLB 覆盖提升 512 倍
- KV Cache:THP 可以动态填充,内核 khugepaged 线程会自动合并连续小页
- 解码工作区:临时缓冲区不适合 THP,等待 khugepaged 扫描带来不必要的延迟
实测数据:在 200GB 工作集的 Llama-2-70B 推理测试中:
| 配置 | Tokens/sec | TLB miss rate | p99 Latency |
|---|---|---|---|
| 4KB pages only | 85.3 | 18.7% | 420ms |
| 2MB pages (weights) + 4KB (KV) | 112.1 | 4.2% | 285ms |
| 2MB pages + THP (madvise) | 118.6 | 2.1% | 241ms |
| 1GB pages (weights) + 2MB (KV) | 127.4 | 0.8% | 195ms |
4.3 Kernel Samepage Merging (KSM) 的 AI 场景应用
KSM 通过扫描内存页内容,合并相同内容的 page 为只读共享页。在 AI 推理的多租户场景中,多个推理实例可能加载同一个基础模型权重:
// 启用 KSM 合并相同模型权重页
// /sys/kernel/mm/ksm/run = 1
// /sys/kernel/mm/ksm/sleep_millisecs = 200
// 在推理引擎中标记可合并区域
madvise(model_weights, weights_size, MADV_MERGEABLE);
NVIDIA Triton Inference Server 的多实例模型共享实际上也是类似思路。KSM 通过内核层面的页面去重,可以在不修改推理引擎代码的情况下,让多个推理进程共享同一份模型权重物理内存。
五、性能工程实战:从诊断到优化
5.1 TLB 性能诊断
对于 AI 推理引擎,第一个诊断步骤永远是量化 TLB 的影响:
# 使用 perf 测量 TLB miss 率
perf stat -e \
dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses,\
iTLB-loads,iTLB-load-misses,\
page-faults,minor-faults,major-faults \
-p $INFERENCE_PID
# 使用 Intel PT 追踪 TLB miss 的高延迟事件
perf record -e 'intel_pt//u' -p $INFERENCE_PID -- sleep 10
# 查看进程级别的 Huge Pages 使用
cat /proc/$INFERENCE_PID/smaps | grep -A 10 "2048 kB\|anon_hugepages\|shmem_hugepages"
关键指标:
- dTLB miss rate > 5%:需要考虑 Huge Pages 或 Contiguous PTE 优化
- Major faults > 0:存在磁盘 I/O 延迟,推理引擎应该避免 swap
- Minor faults 持续高:懒分配带来的延迟尖刺,考虑 MAP_POPULATE
5.2 NUMA 感知的页分配
在双路服务器上,AI 推理引擎的内存分配策略直接影响页表遍历的延迟:
// 获取当前 NUMA 节点的本地内存分配策略
// /proc/sys/vm/zone_reclaim_mode = 1 (NUMA-local reclaim)
// 在推理引擎中显式绑定 NUMA 节点
#include <numa.h>
void bind_to_node(int node) {
struct bitmask *mask = numa_allocate_nodemask();
numa_bitmask_setbit(mask, node);
numa_bind(mask);
numa_free_nodemask(mask);
}
// 或使用 libnuma 的内存分配接口
void *local_alloc(size_t size, int node) {
return numa_alloc_onnode(size, node);
}
NUMA 效应如何影响 TLB:当一个物理页被分配到远端 NUMA 节点时,不仅内存访问延迟增加 2-3 倍,TLB miss 导致的额外页表遍历也会访问更远的物理内存。通过 numactl --cpunodebind=0 --membind=0 启动推理进程,可以确保页表和数据的本地化。
5.3 自定义 mm_struct 的 PGTABLE 复制优化
Linux 内核的 fork() 通常会复制整个页表结构(copy_page_range)。对于 AI 推理引擎的 prefill-decode 分离架构,这种开销可以优化:
// Linux 6.5+ 的 maple tree 优化 mm_struct
// include/linux/maple_tree.h
struct maple_tree {
spinlock_t ma_lock;
unsigned int ma_flags;
void __rcu *ma_root;
};
// madvise 标记 AI 推理的常驻权重区(避免 fork 时复制)
madvise(model_weights, weights_size, MADV_DONTFORK);
// 标记 KV CO写时复制区域
madvise(kv_cache_region, cache_size, MADV_PAGEOUT);
MADV_DONTFORK 告诉内核在 fork 时不复制该区域的页表项。这在 Triton 的"模型预加载 + 多个推理 worker 通过 fork 启动"模式中非常有用——模型权重区的页表项可以共享,避免每次 fork 时的大量内存拷贝。
六、前沿趋势:硬件加速的页表遍历
6.1 ARM64 S2FWB(Stage 2 Fault Walker Bypass)
最新一代 ARM64 处理器(Cortex-X4 / A720+)引入了 S2FWB 硬件特性,允许 Stage-2 页表遍历在缓存命中时直接跳过缓存一致性检查,将虚拟化场景下的 TLB 延迟降低约 30%。
这对 AI 推理上云(Kubernetes 上运行推理 Pod)具有深远意义——容器内的推理引擎不再会因为 Hypervisor 的 Stage-2 页表遍历而丢失大量性能。
6.2 Intel EPT(Extended Page Tables)的 AI 优化
Intel VT-x 的扩展页表提供了两级地址转换(GVA → GPA → HPA)。新一代 Intel 处理器支持 EPT 的 Accessed/Dirty bits 由硬件自动更新,这减少了虚拟机退出次数。
对于云上 AI 推理平台,EPT 的 A/D bit 硬件支持意味着:
- 内核页面回收算法可以直接判断 Guest 页面的活跃程度
- 无需截获 EPT page fault 来处理 A/D 更新
- KVM 推理 Pod 的页表管理开销降低约 15-20%
6.3 CXL 内存扩展与页表挑战
CXL 2.0/3.0 允许将 DRAM 扩展到 TB 级别,但 CXL 内存的访问延迟(200-500ns)远高于本地 DRAM(80-100ns)。在页表层面,Linux 通过 NUMA 节点抽象将 CXL 内存建模为远程 NUMA 页:
// 内核中 CXL 内存的 PTE 标记
// include/linux/pgtable.h
#define PTE_CXL_BIT (_AT(pteval_t, 1) << 51) // 假设
// 内存分层策略:热页留在 DRAM,冷页迁移到 CXL
// 通过 /sys/devices/system/node/nodeX/hugepages/ 配置分层 Huge Pages
AI 推理引擎可以利用这种分层:将当前活跃的 KV Cache 页保留在本地 DRAM,将未活跃的上下文历史页迁移到 CXL 扩展内存。通过 migratepages 命令或 move_pages() 系统调用实现页的跨层级迁移。
七、总结:AI 推理引擎的页表优化实践
回顾全文,AI 推理引擎的页表优化实践可以归纳为以下几个层次:
L1 - 硬件选型:选择支持 5 级页表的 CPU(x86-64 LA57 / ARM64 52-bit VA),确保地址空间不成为瓶颈。优先选择 TLB 覆盖更大的架构(大小核不对称 TLB L2 > 对称 TLB)。
L2 - 页大小策略:模型权重使用 2MB 或 1GB Huge Pages,KV Cache 区域使用 THP + madvise,解码工作区使用默认 4KB 页 + MAP_POPULATE 预填。
L3 - NUMA 本地化:通过 numactl 或 mbind() 确保推理进程的页表和数据在同一 NUMA 节点。对 CXL 远程内存使用分层管理。
L4 - KSM 共享:多实例推理时启用 KSM 合并相同的模型权重页,减少物理内存占用和页表项数量。
L5 - 持续监控:使用 perf、bpftrace 或 eBPF 脚本持续监控 TLB miss rate、page fault 率和 Huge Pages 利用率,动态调整策略。
页表遍历是一个看似底层、实则影响深远的系统问题。在 AI 推理这个把"大内存 + 高吞吐 + 低延迟"需求推到极致的场景中,从硬件抽象层开始的每一级优化都可能带来可观的性能提升。当你为推理引擎的吞吐量焦虑时,不妨先看看 dTLB-load-misses——也许瓶颈不在 GPU 的算力,而在 MMU 的页表遍历上。

发表评论 取消回复