Linux 7.3 合并显存超卖优化补丁
Linux 7.3 提升显存不足时的性能表现
Earlier this year, I blogged about work I did to improve VRAM management for games. Now, after many months of floating around in mailing lists, the kernel patches are finally merged upstream and queued for Linux 7.3! Hooray!
今年早些时候,我写了一篇博客,讲述我为改善游戏VRAM管理所做的工作。现在,经过数月在邮件列表中的讨论,内核补丁终于被合并到上游,并计划用于Linux 7.3!万岁!
To celebrate, let’s look a bit deeper at one sentence I wrote in my previous post:
为了庆祝,让我们更深入地看看我在上一篇帖子中写的一句话:
[Games] should perform much more stable - as long as the game itself doesn’t use more VRAM than you actually have.
[游戏]应该表现得更稳定——只要游戏本身使用的VRAM不超过你实际拥有的量。
So, one may ask: What if they do, in fact, use more VRAM than you actually have?
那么,有人可能会问:如果它们确实使用了超过你实际拥有的VRAM,会发生什么?
Typical expectations for this seem to be that once this happens you’re pretty much screwed. Games will start crashing left and right, performance plummets to unplayable levels, a good gaming experience becomes impossible.
对此的典型预期似乎是,一旦发生这种情况,你就基本完蛋了。游戏会开始到处崩溃,性能骤降至无法游玩的程度,良好的游戏体验变得不可能。
But is that really just an unavoidable fact of life? What really makes running out of VRAM suck so hard? And, most importantly: How can we make it suck as little as possible?
但这真的是生活中不可避免的事实吗?是什么让VRAM耗尽如此糟糕?最重要的是:我们如何让它尽可能不那么糟糕?
Setting expectations
设定预期
In theory, running out of VRAM should exclusively be a performance issue, not a stability one. Support for overcommitting VRAM has existed for as long as GPU drivers have: If the driver overcommits VRAM, you are generally allowed to request as much VRAM as you’d like, and you’ll get as much as the kernel driver decides it can fit into the physical memory that exists on GPU.
理论上,VRAM耗尽应该纯粹是性能问题,而不是稳定性问题。对VRAM过度承诺的支持在GPU驱动存在之初就已存在:如果驱动过度承诺VRAM,你通常可以随意请求任意多的VRAM,而你会得到内核驱动决定能放入GPU上物理内存的量。
On the performance side, the big-picture reason for bad performance when you run out of VRAM is fairly simple. As soon as the game requests more VRAM than is physically present, some of the game’s memory will have to be moved/evicted to CPU RAM instead. For the GPU, accessing CPU RAM is much slower than VRAM: Not only is CPU RAM slower than a dedicated GPU’s VRAM in general, all memory accesses also have to go over the PCI bus. The PCI bus adds latency and is typically also the limiting factor in bandwidth when fetching from CPU memory.
在性能方面,当VRAM耗尽时性能不佳的大局原因相当简单。一旦游戏请求的VRAM超过物理存在的量,游戏的部分内存就必须被移动/驱逐到CPU RAM中。对于GPU来说,访问CPU RAM比VRAM慢得多:不仅CPU RAM通常比专用GPU的VRAM慢,所有内存访问还必须通过PCI总线。PCI总线增加了延迟,并且通常也是从CPU内存获取数据时的带宽限制因素。
Due to PCI speed limitations, there are some truly unavoidable performance constraints when overcommitting VRAM. Assuming the GPU is hooked up via a PCIe 4.0x16 connection, you get a little less than 32GiB/s of bandwidth. Each millisecond, that PCIe bus can transfer ~32.2MiB of data. For a minimum framerate of 30 frames per second (33.3ms per frame), the absolute maximum amount of data the GPU is able to access is ~1,075.5MiB, a tiny bit over 1GiB of data. In other words, if so much memory gets evicted that the GPU needs to fetch more than 1GiB from evicted memory in one single frame, it is simply impossible to still hit 30 FPS.
由于PCI速度限制,在过度承诺VRAM时存在一些真正不可避免的性能约束。假设GPU通过PCIe 4.0x16连接,你获得略低于32GiB/s的带宽。每毫秒,该PCIe总线可以传输约32.2MiB的数据。对于最低帧率30帧每秒(每帧33.3毫秒),GPU能够访问的绝对最大数据量约为1,075.5MiB,略超过1GiB的数据。换句话说,如果被驱逐的内存太多,以至于GPU需要在一帧内从被驱逐的内存中获取超过1GiB的数据,那么要维持30 FPS就根本不可能了。
Not all memory is equal
并非所有内存都是平等的
At the same time, just reading a little bit of CPU memory on the GPU is not immediately a death sentence for performance. In fact, GPU drivers sometimes decide to let things like command buffer data and related allocations live in CPU RAM even when there’s plenty of VRAM available! Whenever the GPU executes these commands, it has to access CPU memory, and yet in these cases everything runs completely fine. So what makes these accesses different - why are they fine and yet running out of VRAM seems catastrophic?1
同时,在GPU上仅读取少量CPU内存并不会立即导致性能灾难。实际上,GPU驱动程序有时会决定让命令缓冲区数据及相关分配驻留在CPU RAM中,即使有充足的VRAM可用!每当GPU执行这些命令时,它必须访问CPU内存,但在这些情况下一切运行完全正常。那么,这些访问有何不同——为什么它们没问题,而VRAM耗尽似乎却是灾难性的?
One thing that influences the calculus significantly is caching. Since the access latency in case of a cache hit is the same regardless of whether the cached memory lives on CPU or GPU, the high initial cost of fetching over the PCI bus can be amortized by cache hits (to some extent). We can estimate latency differences between fetching CPU RAM and VRAM by writing microbenchmarks that measure access latency for different buffer sizes (using an adversarial access pattern to minimize cache hitrates as far as possible). The result you get may look something like this (captured on RDNA3):
显著影响这一计算的一个因素是缓存。由于缓存命中时的访问延迟无论缓存内存位于CPU还是GPU上都是相同的,通过PCI总线获取的高初始成本可以在一定程度上通过缓存命中来摊销。我们可以通过编写微基准测试来估算CPU RAM和VRAM获取之间的延迟差异,这些测试测量不同缓冲区大小的访问延迟(使用对抗性访问模式以尽可能降低缓存命中率)。你得到的结果可能类似于这样(在RDNA3上捕获):
As expected, if the buffer fits into L2 (or any higher-level cache), access latencies are exactly the same for memory backed by CPU RAM and memory backed by VRAM, because the data gets fetched directly from cache in either case. At a size of 6MB (the L2 cache size on RDNA3), CPU memory latencies go up to about 2400 cycles per access, while device memory latencies stay within the same rough ballpark. Note that VRAM accesses also go through the Infinity Cache, but CPU memory accesses do not (they hit PCIe directly on an L2 miss). I suspect this is because the Infinity Cache sits directly on top of VRAM, so any access that doesn’t hit VRAM also doesn’t reach the Infinity Cache.
正如预期,如果缓冲区适合L2(或任何更高级别的缓存),CPU RAM和VRAM支持的内存的访问延迟完全相同,因为在这两种情况下数据都直接从缓存获取。在6MB大小(RDNA3上的L2缓存大小)时,CPU内存的延迟上升到每次访问约2400个周期,而设备内存的延迟保持在大致相同的范围内。注意,VRAM访问也经过Infinity Cache,但CPU内存访问不经过(在L2未命中时直接命中PCIe)。我怀疑这是因为Infinity Cache直接位于VRAM之上,因此任何未命中VRAM的访问也不会到达Infinity Cache。
Obviously, memory doesn’t start off with being cached anywhere, so the first access will still have considerably higher latency. Also, losing the Infinity Cache definitely hurts as well: PCIe fetches seem to have somewhere around 7.3x as much latency than an Infinity Cache hit, and around 4.6x as much latency as a fetch from VRAM. This increased latency needs really high cache hitrates to fully amortize the cost of going over PCIe. That means there is only a small set of use cases where using CPU memory has such minuscule slowdowns that you’d actively decide to use it in favor of VRAM when you have the choice. When you’re evicting memory from VRAM, there will almost unavoidably be at least some degree of slower performance.
显然,内存一开始并不会被缓存到任何地方,因此首次访问的延迟仍然会显著更高。此外,失去Infinity Cache确实也会造成影响:PCIe获取的延迟大约是Infinity Cache命中的7.3倍,比从VRAM获取的延迟高约4.6倍。这种增加的延迟需要非常高的缓存命中率才能完全摊销通过PCIe访问的成本。这意味着,只有少数使用场景中,使用CPU内存的减速如此微小,以至于在有选择时你会主动决定使用它而不是VRAM。当从VRAM中驱逐内存时,几乎不可避免地会有至少一定程度的性能下降。
Still, even though slowdown is unavoidable, there is going to be memory where eviction matters more and memory where eviction has a lesser effect on overall perf. Memory that is accessed in very cache-friendly ways is not affected by the slowdown of CPU RAM as much. If the access patterns aren’t cache-friendly but the memory isn’t accessed very often, things may also still be fine since the GPU only rarely needs to actually fetch data from CPU RAM. There might be many memory allocations where the GPU will only access a small part of the total allocation size, and never even read the rest. If these allocations were to be evicted, you might evict multiple GiBs of data, but still remain well below the 1GiB hard limit of data that is actually accessed per frame.
尽管如此,即使减速不可避免,有些内存的驱逐影响更大,而有些内存的驱逐对整体性能影响较小。以非常缓存友好的方式访问的内存受CPU RAM减速的影响较小。如果访问模式不缓存友好,但内存访问频率不高,情况也可能还好,因为GPU很少需要实际从CPU RAM获取数据。可能有许多内存分配,GPU只会访问总分配大小的一小部分,甚至永远不会读取其余部分。如果这些分配被驱逐,你可能驱逐了数GiB的数据,但仍远低于每帧实际访问的1GiB硬限制。
All of these variables make it surprisingly hard to predict how performance actually pans out in practice when memory is being evicted. But in short: Depending on how much the evicted memory gets accessed and how well these accesses cache, you might just be able to run out of VRAM without (completely) ruining performance!
所有这些变量使得预测内存被驱逐时实际性能表现变得异常困难。但简而言之:根据被驱逐内存的访问频率以及这些访问的缓存友好程度,你可能能够在VRAM耗尽的情况下运行,而不会(完全)破坏性能!
Confronting reality
面对现实
We’ve theorycrafted ourselves all the way towards having performant VRAM overcommitment now. Great! Let’s just boot up SteamOS, start some game and crank up the setti-
我们已经通过理论推导,实现了高性能的VRAM超量使用。太好了!让我们启动SteamOS,开始某个游戏,调高设置——
radv/amdgpu: Not enough memory for command submission.
radv/amdgpu: 命令提交内存不足。
oh.
哦。
As it turns out, running out of VRAM in practice does carry plenty of stability issues with it.
事实证明,在实际中VRAM耗尽确实会带来许多稳定性问题。
This error isn’t quite like a regular “couldn’t allocate, out of memory” error, though. Note that the message specifically complains about command submission: RADV prints this message when the kernel returns -ENOMEM when trying to submit commands2, but merely submitting commands does not allocate any new resources! All the command buffers were allocated in advance, and clearly their allocation succeeded. Even though all memory was successfully allocated, using it in a GPU submission suddenly results in “out of memory” errors being thrown.
不过,这个错误并不像普通的“无法分配内存,内存不足”错误。请注意,消息特别提到了命令提交的问题:当内核在尝试提交命令2时返回-ENOMEM,RADV会打印这条消息,但仅仅提交命令并不会分配任何新资源!所有命令缓冲区都是预先分配的,显然它们的分配是成功的。即使所有内存都成功分配,在GPU提交中使用它却突然抛出“内存不足”错误。
It’s time for another kernel adventure! Surely getting the kernel to accept the submission can’t be that hard - after all, the kernel already accepted all the allocations3!
是时候再来一次内核冒险了!让内核接受提交肯定没那么难——毕竟,内核已经接受了所有的分配3!
The horrors of kernel locking
内核锁定的恐怖
One thing the amdgpu driver has to do on every submission, before it can direct the GPU to start executing commands, is to make sure that all memory that may potentially be referenced by the GPU commands is accessible. With more modern bindless graphics APIs, you have to assume all allocated memory may at some point get referenced. Therefore, amdgpu will try to make sure all allocated memory is also accessible.
amdgpu驱动在每次提交时,在指示GPU开始执行命令之前,必须确保所有可能被GPU命令引用的内存都是可访问的。随着更现代的绑定无关图形API的出现,你必须假设所有分配的内存都可能在某时被引用。因此,amdgpu会尝试确保所有分配的内存也是可访问的。
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力