随着时间的推移,CPU 的计算能力呈指数级增长,而内存容量的需求增长得更快。随着 CPU 和 GPU 计算能力的激增,我们发现 AI 训练等许多工作负载现在都受到可寻址系统内存不足的限制。虽然虚拟内存(基于 SSD 的交换空间)可能在操作系统层面有所帮助(它可以防止系统因内存不足问题而崩溃),但对于在高性能工作负载中扩展内存容量而言,它不是一个理想的解决方案。
针对这一问题,一项技术解决方案是引入 Compute Express Link™(CXL),该技术允许在多个计算节点之间连接与共享内存池,使得内存可扩展性呈数量级提升(相较于本地 DRAM 而言)。它还需要根据性能和局部性对内存进行细粒度的分层,涵盖处理器缓存和 GPU 本地内存(如高带宽内存(HBM)),以及速度较慢的远内存(后者需要采用更复杂的一致性结构)。
一般来说,这种内存容量的扩展只与 DRAM + CXL 有关;存储则基本不受影响,NVMe SSD 的运行不应该受到影响,对吧? 实际情况并非完全如此。使用 SSD 时应了解分层内存,并可对其进行优化以提高高性能工作负载的性能和/或延迟。此用例中的一项 SSD 优化需要 ATS 及其相关 ATC 的支持,我们将在此处讨论。
什么是 ATS/ATC?
ATS/ATC 最相关的用途是用在虚拟化系统中。它们也可以用于非虚拟化系统,但简单说来,ATS/ATC 的工作原理是,使用 SrIOV 等标准技术,通过跟踪直接分配虚拟化系统中的虚拟机(VM)的数据路径来工作。参考示意图如下所示:
虚拟化的一个关键特点是,访客虚拟机不知道它在虚拟化环境中运行。系统设备(如 SSD)认为它们正在与单个主机通信,而不是与大量 VM 通信,因此它们在 SSD 上无需额外逻辑即可正常工作。
这种方法的一个结果是内存寻址。访客操作系统设计为在专用系统上运行,因此它认为自己正在使用系统内存地址(SVA = 系统虚拟地址;SPA = 系统物理地址),但实际上它在 VM 空间中运行,由一个虚拟层管理程序管理,提供的访客地址是本地于虚拟机的,这些地址与系统的地址映射完全不同。访客操作系统认为它正在使用 SVA 和 SPA,但实际上它正在使用访客虚拟地址(GVA)和访客物理地址(GPA)。
在从本地(访客)寻址方案转换到全局(系统)寻址方案的过程中,必须小心谨慎。处理器有两种转换机制:内存管理单元(MMU)和 IOMMU,前者支持程序直接访问内存时的地址转换(与 SSD 无关),后者支持所有 DMA 传输的地址转换(这是这里的关键方面)。
如图 1 所示,每当 VM 的访客操作系统想要从 SSD 执行读取时,它必须提供 DMA 地址。它以为提供的是 SPA,实际上却是 GPA。作为 DMA 地址发送到 SSD 的只是 GPA。当 SSD 发出请求的数据时,它会发送 PCIe 数据包(称为事务层数据包 [TLP],通常大小高达 512 B),并附带 GPA。在这种情况下,IOMMU 会检查传入的地址,识别出这是 GPA,查找其转换表以确定相应的 SPA,并将 GPA 替换为 SPA,以便可以使用正确的内存地址。
我们为什么关心 NVMe 的 ATS/ATC?
系统中拥有许多 SSD 或 CXL 设备可能会导致地址转换风暴,从而堵塞 IOMMU,造成系统性能瓶颈。
例如,现代 SSD 可以执行高达 600 万次 4 KB IOPS。鉴于 TLP = 512 B,每个 IO 需要拆分为 8 个 TLP,因此有 4800 万个 TLP 需要转换。鉴于 IOMMU 每 4 台设备实例化一次,那么每秒需要转换 1.92 亿个 TLP。这是一个很大的数字,但由于 TLP 的大小“高达 512 B”(也可能更小),情况可能会更糟。如果 TLP 较小,则地址转换数量会相应地增加。
我们需要找到一种方法来减少转换数量。这就是 ATS 的作用:作为一种机制,它可提前请求地址转换,在转换有效期间可重复使用这些转换结果。鉴于操作系统的页面大小为 4 KB,每个转换结果可用于 8 个 TLP,从而成比例减少转换数量。但是页面可以是连续的,在大多数虚拟化系统中,下一个有效粒度是 2 MB,因此每个转换结果可以用于 8*2M/4K = 4096 个连续的 TLP(如果所用的 TLP 小于 512 B,则更多)。这使得 IOMMU 必须提供的转换数量可以从约 2 亿减少到远低于 10 万,从而减轻了堵塞的风险。
在 NVMe 环境中为 ATS/ATC 建模
在 NVMe 中,针对每个命令,提交队列和完成队列(SQ 和 CQ)的地址只使用一次。我们是否应该保留此类(静态)转换的副本? 一定要保留。这正是 ATC 的作用:保留最常见转换的缓存副本。
此时,主要问题是了解 SSD 将接收哪些 DMA 地址模式,以便围绕它们设计 ATS/ATC。遗憾的是,关于这些模式的数据或文献几乎不存在。
为了解决这个问题,我们开发了一款工具,用于追踪 SSD 接收到的所有地址,并将它们存储起来,以便用于建模。为了有意义,数据需要来自实际运行应用程序的一些可接受表示,并且包含足够多的数据点,以便代表一个用于缓存实现的有效数据集。
我们为一系列数据中心应用程序选择了常见的工作负载基准测试,运行它们,并为每个 20 分钟的段获取 IO 跟踪数据。这导致每条跟踪数据包含了数以亿计的数据点,用于为建模工作提供支持。
下图展示了一个数据收集示例,通过在 RocksDB 上使用 XFS 文件系统运行 YCSB(Yahoo Cloud Server Benchmark)来获得:
针对存储系统的数据 ATC 评估:
- 表征方法:
- 假设 VM 正在运行标准工作负载。
- 跟踪每个工作负载的唯一缓冲区地址
- 将地址映射到 STU(2 MB)的低页面
- 构建 ATC 的 Python 模型
- 将跟踪数据重放到模型以验证命中率
- 对尽可能多的工作负载和配置重复以上步骤
观察结果:正如预期的那样,与单个图像相比,在多个 VM 中,局部性较低,但并非线性扩展(即当 #VM 数量增加到 16 倍时,缓存大小并没有相应地增加到 4 倍)
为了计算缓存需求,我们构建了一个 Python 模型来模拟缓存,重放了整个跟踪记录(共包含数亿个条目)并分析了缓存行为。通过这个模型,我们能够模拟缓存大小(行数)、驱逐策略、STU 大小以及与建模相关的任何其他参数的变化。
我们针对每个基准测试分析了约 1.5 亿到 3.7 亿个数据点,发现使用的唯一地址数量通常在数万个左右,这对于缓存大小的调整来说是一个很好的结果。如果我们进一步将它们映射到最常用的 2 MB 页面(最小传输单元,即 STU),页面数量将减少到几百个到几千个。
这表明缓冲区的重复使用率非常高,意味着即使系统内存达到 TB 级别,用于 IO 的数据缓冲区数量仍在 GB 级别,使用的地址数量也在数千个范围内,这使得该方法成为了一个不错的缓存候选方案。
我们担心高地址重用率是由特定系统配置中的局部性引起的,因此我们针对几个不同的数据应用基准测试进行了额外的测试。下表比较了上述某个 YCSB/RocksDB/XFS 基准测试与使用 XFS 的 Microsoft SQL Server 上的 TPC-H 基准测试,后者是一种截然不同的基准测试:
与 TPC-H 基准测试的相关性:
- IO 分布迥然不同:
- 有 3.2 倍多的唯一地址……
- ……但分别在 70% 的 STU 中 -> 局部性更高
- 当缓存大小约 64 行时,其与 RocksDB 的缓存命中率趋于一致
两者的数据跟踪完全不同,但如果缓存大小足够大(比如超过微不足道的 64 行),它们会收敛到相同的命中率。
类似发现已通过其他几个基准测试得到了验证。为了简洁起见,此处未详述。
大小依赖关系:
- 使用的基准测试:YCSB WL B 与 Cassandra 结合使用
- 缓存:4 路组相联
- 算法:时间片轮转
- 观察结果:
- 正如预期的那样,命中率高度取决于 STU 大小
- STU 越大,命中率越好
- 并非所有的数据都是平等的:每个 NVMe 命令都需要访问 SQ 和 CQ,因此此类地址对命中率的影响很大
数据固定和驱逐算法建模
我们还可以模拟不同算法对特殊数据固定(提交队列和完成队列)和数据替换的影响。结果如以下两幅图所示:
第一组图表验证了缓存对行大小、STU 大小的依赖关系,以及将 SQ 和 CQ 固定到 ATC 是否会有影响。对于相对较小的缓存来说,答案显然是“会有影响”。两组曲线的形状虽然非常相似,但包含 SQ/CQ 缓存的曲线在缓存较小时,其命中率起点明显更高。例如,在 STU = 2 MB 且仅有 1 个缓存行的情况下(尽管在实践中非常罕见,但有助于说明问题),不包含 SQ/CQ 缓存的命中率低于 10%,但固定住 SQ/CQ 时,命中率接近 70%。因此,这是一个很好的做法。
关于缓存性能对所选驱逐算法的敏感性,我们测试了最近最少使用(LRU)、时间片轮转(RR)和纯随机(Rand)这三种算法。如下图所示,影响可忽略不计。因此,应选择更简单、最高效的算法。
算法依赖关系:
- 使用的基准测试:
- YCSB WL B 与 Cassandra 结合使用
- 更大集合的一部分
- 关联性:全关联和 4 路组相联
- 驱逐算法:LRU、随机和时间片轮转
- 结果:
- 替代算法不会产生任何明显差异
- 选择最简单的实现可能是最有效的方法
结论
那么,我们能根据这些发现做些什么? 这种方法存在哪些差距?
这提供了一种途径,通过测量参数来定量设计 ATC 缓存,以便可以正确调整性能和设计影响。
这种方法的注意事项如下:本报告仅代表我们的初步分析,并非最终结论。例如,从 ATS/ATC 中获益最大的应用程序通常运行在虚拟化环境中,但是跟踪数据并非来自该环境。数据展示了如何弥合该差距,但这种方法更多地停留在理论原则层面,尚不能应用。因此,在进行每个 ASIC 设计时,都需要仔细考虑并做出适当的权衡。另一个要解决的差距是 ASIC 的设计需要将近 3 年时间,而新产品能够使用该设计的时间是大约 2 到 3 年,其实际使用寿命也大致相当。预测 3-7 年后工作负载的情况是一项具有挑战性的任务。那时将有多少 VM 在多少内核上运行?需要使用多少内存?
在测试中,我们找到了一条量化解决方案的道路。尽管这些未知因素看起来令人担忧,但在建模界并不罕见,任何新的 ASIC 设计都需要相应地解决这些问题。