解决异构内存割裂:Linux HMM 底层框架基础解析
设备上独立内存的管理通常是自成体系的,常见具有独立内存的设备一般是 GPU,我们将其称之独立显存。独立显存的分配往往是通过 Vendor 自己实现的 API,即没有办法兼容内核中原有的内存分配方式,例如 Page Alloc 或 kmalloc 等。这会导致一个问题现象是,系统内存与独立显存的空间没办法统一化,随着 GPU 的软件生态逐渐强化,GPU 与 CPU 间的内存共享需求变得越来越强烈,像异构计算、AI 训推等场景,在一轮算子运行之后想更新下相应输入参数再开启下一轮推理,这期间就会涉及到共享内存,如何将共享内存实现的更加简易和框架化?HMM (Heterogeneous Memory Management) 应运而生。设计背景
HMM 的创作者是 Jérôme Glisse,于 2017 年合入到 Next 分支,我们可以大致梳理一下他是在什么背景下决定开发一套 HMM 框架的。设备访问系统内存效率
GPU 一般拥有多种访存通路,包括独立显存的寻址、P2P 卡间访问、系统内存的访问等,既然可以访问系统内存,还需要一套复杂的 HMM 框架做什么,直接用内核分配出来的内存页面映射给到 GPU 硬件就好了,为什么还要大费周章?抱歉,访存通路虽然存在,但作为高速并行计算设备,它需要极高的访存带宽,如果访问系统内存,通路涉及 PCIE 或其它总线,最重要的是系统内存的介质自身带宽上限也有限,根本无法支撑 GPU 带宽需要,和独立显存带宽相比,性能差距可达 10 倍以上。数据共享操作流程冗长
对于 GPU 高频访问的数据,需要从 CPU 侧系统生成后再拷贝至独立显存中,拷贝的方法一般是通过 GPU 内部集成的 DMA IP,那么我们大致可以将数据存储的链路呈现出来:CPU 生产数据 -> 分配独立显存 -> 触发 GPU DMA 拷贝数据,也就是说,但凡涉及到 GPU 使用 CPU 生产数据的场景均涉及到这个链路。如果我们能够做好抽象,将其封闭为用户态级 HAL 库,那么用户在使用起来会变得方便许多,但用户态拥有很多操控 GPU 加速计算的库,像 CUDA、CUB、OpenCL 等等,如果都包含抽象库,这会将操作变得繁多,结构复杂。数据对象存储结构不一
和 GPU 共享的数据可以拥有多种形态,如果是图片、数组等紧凑密集形态,数据的拷贝可以比较简单,大部分可以触发一次 DMA 拷贝完成。但如果共享的是稀疏数据类型,像链表、树等这些,触发 DMA 数据传输的难度会加大,操作难度大且效率低。结合对以上对现状的考量,设计出一套通常的异构内存架构将变得十分迫切。原理分析
为了优雅的实现内存和 GPU 共享,HMM 实现内存的拷贝几乎是“无感”的,其中的内存拷贝技术作者将其称为 Address space mirroring。至于无感,是指不再需要显式触发 DMA 拷贝,可以通过 CPU 或 GPU 在使用时按需分配内存或独立显存,并且自动完成内存到独立显存的拷贝。提到按需分配,可以联想到 Page Fault,事实确实是这样,CPU 和 GPU 均可以通过各自的 Page Fault 主动地将对应内存进行更新,这个更新状态在内核中一般称之为 Present,而内存数据在两端间的迁移主要就是通过核心的 Migrate 机制来完成的。文末总结
本篇作为 Linux HMM 异构内存管理的基础介绍,叙述了 HMM 诞生的背景和简单要原理分析,后续将分成多篇文档来进一步挖掘 HMM 的深层技术构成,包含 Zone Device / HugePage / MMU Notifier / UVM 等等,欢迎关注订阅。https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/mm/hmm.c?id=133ff0eac95b7dc6edf89dc51bd139a0630bbae7