2、内存管理基础:虚拟内存、物理内存、页表、TLB、内存碎片等核心概念

大模型推理与训练过程中的显存(GPU Memory)或内存(CPU RAM)管理,本质上是对操作系统与硬件内存管理机制的深度应用。无论是使用 PyTorch 的 caching allocator,还是手动管理 KV Cache 的显存池,其底层都绕不开虚拟内存、物理内存、页表、TLB 以及内存碎片这些核心概念。理解这些基础机制,是后续进行内存池设计、显存碎片优化和调试的前提。本章将系统梳理这些概念,并重点剖析它们在大模型部署场景下的具体表现与影响。

大模型部署内存管理核心机制总览 虚拟内存 (Virtual Memory) 进程独立地址空间 连续逻辑地址 (如 0x7f...) 按需分配 / 惰性加载 物理内存 (Physical Memory) 实际硬件资源 (DRAM/HBM) 物理页框 (Page Frame) 有限且共享 页表 (Page Table) 虚拟页号 → 物理页框号 多级页表 (4级) TLB (快表) 缓存最近页表项 映射 翻译 加速 内存碎片 (Memory Fragmentation) 外部碎片 空闲块不连续,无法分配大块 内部碎片 分配粒度大于请求,浪费空间 大模型显存池管理 = 在虚拟/物理内存机制之上,优化页表映射与分配策略,降低碎片化

2.1 虚拟内存与物理内存:大模型地址空间的基石

核心原理:虚拟内存是操作系统为每个进程提供的独立、连续、逻辑上的地址空间抽象。对于大模型推理服务而言,进程看到的地址空间(如 PyTorch 中 tensor.data_ptr() 返回的地址)是虚拟地址,而非真实的硬件地址。物理内存(GPU 上的 HBM 或 CPU 的 DRAM)则是实际存储数据的硬件资源。操作系统通过页表将虚拟地址动态映射到物理地址。这种隔离机制带来的关键优势是:进程间内存隔离(一个模型的崩溃不会直接拖垮另一个模型)和超量分配(虚拟内存可以大于物理内存,按需换入换出)。

大模型部署中的表现:当加载一个 70B 参数的模型时,其权重文件约 140GB(FP16)。在 GPU 上,CUDA 驱动会为进程创建虚拟地址空间,并建立页表映射。但实际物理显存可能只有 80GB,此时若开启 Unified Memory 或使用 CPU offload,虚拟内存机制允许程序“假装”拥有全部 140GB 地址空间,实际数据在 GPU 显存和 CPU 内存之间按页迁移。理解这一点,有助于解释为何有时 `nvidia-smi` 显示的显存占用远小于模型文件大小——因为部分页尚未被访问,尚未映射到物理显存。

调试经验:在排查显存溢出(OOM)时,不要仅看 `nvidia-smi` 的物理显存占用。应使用 `torch.cuda.memory_stats()` 查看 `reserved_bytes` 和 `allocated_bytes` 的差异。`reserved_bytes` 是 CUDA 驱动从物理显存中划给进程的块,而 `allocated_bytes` 是 PyTorch 实际使用的。两者差值过大,说明存在缓存但未使用的显存,这是内存池管理优化的重点方向。

2.2 页表与 TLB:地址翻译的性能瓶颈

核心原理:页表是虚拟地址到物理地址的映射表。现代 64 位系统使用 4 级页表(如 x86-64 的 PGD/PUD/PMD/PTE),每次访问内存都需要 4 次额外的内存读取来完成地址翻译。为了加速这一过程,CPU/GPU 内部设计了 TLB(Translation Lookaside Buffer),即页表项的缓存。TLB 命中时,地址翻译几乎零开销;TLB 未命中时,需要遍历多级页表,产生数十纳秒的延迟。

大模型场景的挑战:大模型推理时的内存访问模式具有稀疏性和大跨度特点。例如,KV Cache 的访问通常集中在最近生成的 token 对应的内存区域,但 Attention 计算需要随机访问所有历史 token 的 KV 块。如果页表映射的粒度是 4KB,那么一个 8MB 的 KV Cache 块就需要 2048 个页表项,极易导致 TLB 未命中。为此,CUDA 和最新操作系统引入了大页(Huge Pages)机制,如 2MB 或 1GB 页。使用大页可以将页表项数量减少 512 倍,显著提升 TLB 命中率。

页大小 页表项数量 (8MB 区域) TLB 覆盖范围 适用场景
4KB 2048 约 512KB (典型 TLB 64项) 通用场景,碎片小
2MB 4 约 128MB KV Cache、权重加载
1GB 1 约 64GB 超大模型权重驻留

操作流程:在 PyTorch 中启用大页支持,通常需要设置环境变量 `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`,该配置允许 CUDA 分配器使用更大的虚拟内存段(类似大页效果)。对于 CPU 场景,可以通过 `libhugetlbfs` 或 `mmap` 的 `MAP_HUGETLB` 标志来分配大页内存。

风险提示:大页虽然减少了 TLB 未命中,但增加了内存分配的最小粒度。例如,2MB 大页意味着即使只请求 10KB 内存,也会占用 2MB 物理页,导致内部碎片增加。在显存池管理中,需要权衡 TLB 性能与碎片率。建议对 KV Cache 这类大块、长生命周期的内存使用大页,对临时激活值(Activation)使用常规 4KB 页。

2.3 内存碎片:显存池管理的头号敌人

核心原理:内存碎片分为外部碎片和内部碎片。外部碎片指空闲内存块不连续,导致无法满足大块连续内存请求;内部碎片指分配器返回的内存块大于请求大小,造成浪费。在大模型推理中,外部碎片尤为严重。由于 Attention 计算的 KV Cache 大小随序列长度动态变化,频繁的申请和释放不同大小的内存块,会在显存池中留下大量“空洞”。

案例拆解:假设一个推理服务同时处理 4 个请求,显存池初始有 40GB 连续空闲。请求 A 申请 10GB,请求 B 申请 8GB,请求 C 申请 12GB,请求 D 申请 10GB。此时显存池被完全占用。当请求 B 完成并释放 8GB 后,请求 E 需要 9GB 的 KV Cache。由于 8GB 空洞小于 9GB,分配失败,即使总空闲内存仍有 8GB。这就是典型的外部碎片问题。

# 模拟显存碎片化场景
import torch

# 模拟分配 4 个块
blocks = []
for size in [10, 8, 12, 10]:
    blocks.append(torch.empty(size * 1024**3, dtype=torch.float16, device='cuda'))

# 释放中间块
del blocks[1]
torch.cuda.empty_cache()

# 尝试分配 9GB 块 - 可能失败或触发碎片整理
try:
    new_block = torch.empty(9 * 1024**3, dtype=torch.float16, device='cuda')
    print("分配成功")
except torch.cuda.OutOfMemoryError:
    print("分配失败:外部碎片导致")

解决方案与复盘:业界主流方案是内存池分块管理(如 vLLM 的 PagedAttention)。其核心思想是将 KV Cache 划分为固定大小的块(如 16KB/块),通过页表(块表)将不连续的物理块映射为连续的虚拟地址。这样,即使物理块分散,逻辑上依然连续,彻底消除了外部碎片。内部碎片则通过调整块大小来控制——块越小,碎片越少,但页表项越多,TLB 压力越大。

实战建议:在部署大模型服务时,优先使用支持 PagedAttention 的推理框架(如 vLLM、SGLang)。如果必须使用原生 PyTorch,建议预先估算最大并发数和最大序列长度,一次性预留足够大的 KV Cache 池,避免动态扩容导致的碎片。同时,定期调用 torch.cuda.memory_snapshot() 分析显存池状态,识别碎片化程度。

2.4 常见误区与总结复盘

误区一:认为显存占用等于模型参数大小。实际上,推理时的显存占用还包括激活值、梯度(训练时)、优化器状态(训练时)以及 CUDA 上下文(约 500MB-1GB)。误区二:盲目使用 `torch.cuda.empty_cache()`。该操作会释放缓存块,但代价是后续分配需要重新向驱动申请,增加延迟。在推理服务中,应避免频繁调用。误区三:忽略 TLB 性能。在 CPU 上做模型推理时,如果内存访问模式导致 TLB 频繁未命中,性能可能下降 20%-30%。

总结复盘:内存管理基础决定了上层内存池设计的边界。虚拟内存提供了灵活性,页表和 TLB 决定了访问效率,碎片化则直接影响显存利用率。在实际调试中,建议按以下顺序排查:先看物理显存是否充足(`nvidia-smi`),再看进程内部分配情况(`torch.cuda.memory_summary()`),最后分析碎片化程度(`memory_snapshot` 可视化)。只有将这三层机制理解透彻,才能设计出高效、稳定的大模型部署内存池。