来自 Anukari 的呼吁:一个微小的 macOS 细节加速 Anukari
devlog
来自 Anukari 对 Apple 的呼吁
TL;DR: 为了让 Anukari 在所有 Apple silicon macOS 设备上的性能都稳定可靠,我需要和 Apple Metal 团队的人沟通。如果有人能帮我联系到 Apple 内部的合适人选,或者让他们关注我的反馈请求 FB17475838 以及这篇 devlog,那就太好了。 这将是一篇非常长且技术性很强的帖子,所以要么系好安全带,要么趁现在赶紧离开。
背景
Anukari 3D Physics Synthesizer 实时模拟一个大型弹簧-质量模型,用于音频生成。为了支持大量的物理对象,它需要 GPU 来进行模拟。物理代码是 ALU 密集型的,而不是内存密集型的。模拟中所有可变状态都存储在 GPU 的 threadgroup memory 中,这大致相当于手动分配的 L1 缓存,因此速度非常快。
Anukari 的典型用例是在宿主应用程序(例如 Pro Tools 或 Ableton,也称为 Digital Audio Workstation (DAW))中作为 AudioUnit (AU) 或 VST3 插件运行。DAW 为每个音频缓冲区块调用 Anukari,这相当于请求生成/处理 N 个音频样本。对于每个块,Anukari 调用物理模拟 GPU kernel,等待结果,然后返回。
音频缓冲区块系统非常重要,因为 GPU kernel 调度有一定的延迟开销,而且对于实时音频,我们有固定的时间约束。通过将 GPU 调度延迟分摊到例如 512 个音频样本上,延迟可以忽略不计。但是 kernel 本身的运行时间仍然非常重要。
基本问题
Apple 的 macOS 在电源管理方面显然非常出色,而 Apple silicon 硬件旨在支持操作系统实现高能效。
与所有现代硬件一样,Apple silicon 芯片的时钟频率可以降低,以减少功耗。当操作系统检测到给定芯片的处理需求较低(或不存在)时,它可以降低该芯片的时钟频率。这非常棒。
问题在于,由于 Anukari 在 DAW 中运行并与 GPU 交互的方式,macOS 用于确定 GPU 是否有足够的需求来提高其时钟频率的启发式方法不起作用。
考虑下面的图表。CPU 执行一些准备工作,有一个小间隙代表 kernel 调用延迟,然后 GPU 执行大量的工作。最后还有另一个小间隙代表实时裕量。
(题外话:黑板比白板好得多,除非你喜欢吸入有毒烟雾,否则白板才是你的选择。)
我对 macOS 如何决定何时提高 GPU 时钟速度的启发式方法没有任何实际了解,但我可以合理地猜测它依赖于类似 load average 的东西。在上图中,GPU load average 可能只有 60%,因为在音频缓冲区块之间它是空闲的。也许这没有达到提高 GPU 时钟频率的阈值。
但这对 Anukari 来说非常糟糕,因为为了满足实时约束,它需要绝对最低的延迟,这需要最高的 GPU 时钟频率。我不确定 Apple GPU 的时钟频率能降到多低,但它肯定会降到 Anukari 无法使用的程度。
需要明确的是,macOS 对这种情况的处理不佳是可以理解的,因为 GPU 主要用于吞吐量工作流程,例如图形或 ML。GPU 上的音频是全新的,而且目前只有几家公司在做。
你确定时钟频率是问题所在吗?
哦,是的。 幸运的是,Apple 随 Xcode 提供的第一方 Instruments 工具有一个方便的 Metal profiler。 除此之外,这也是我第一次了解到 Anukari 是 ALU 密集型的。
Metal profiler 有一个非常有用的功能:它允许你在分析应用程序时选择 Metal "Performance State"。 这是在 profiler 之外无法配置的。 这也是我第一次发现 GPU 时钟频率是问题所在的原因:Anukari 在 Maximum performance state 下运行良好,在 Minimum performance state 下运行极差。
等等,Anukari 在 macOS 上大部分时间运行良好。这怎么可能?
鉴于上面的解释,这是一个很好的问题。 大多数使用 macOS 的人发现 Anukari 运行良好。 例如,我特意购买了一个基础型号的 Macbook M1 用于开发,这样如果 Anukari 对我来说运行良好,我就知道它对拥有更强大硬件的人来说也运行良好。
那么,为什么 Anukari 在 macOS 上对大多数人来说运行良好呢? 好吧,正如 Steve Jobs 所说,“做海盗胜过加入海军。”
由于无法依靠 macOS 来做正确的事情,我像海盗一样思考,并提出了一个解决方法:在 GPU 上进行音频计算的同时,Anukari 在 GPU 上运行第二个工作负载,该工作负载旨在创建高 load average 并欺骗 macOS 来提高 GPU 时钟频率。 此工作负载经过调整,旨在尽可能少地使用 GPU,同时仍然创建足够大的人工负载来触发时钟启发式方法。
换句话说,它运行一个自旋循环来加热 GPU。 在我的 Macbook M1 上,这完全解决了问题。 Anukari 运行完全可靠。 我将此策略称为“浪费加速”,并在我的 devlog 此处 中详细记录。
需要明确的是,自旋循环是一种不洁的憎恶,我讨厌它有存在的必要。 但是,对于 Anukari 来说,这绝对是 macOS 用户顺利运行所必需的。 而且对于大多数 macOS 用户来说,它运行良好。
因此,通过“浪费加速”策略,问题是什么?
就像我说的那样,在我的 M1 上,一切都运行良好。 但是后来我发布了 Anukari Beta,一些 macOS 用户遇到了问题。 有什么不同?
首先:我不确定。 但是我有几个假设。
奇怪的是,似乎大多数遇到性能问题的用户都在使用 Pro 或 Max Apple 硬件。 这些有额外的 GPU chiplet。 我完全是在推测,但是 Apple 的硬件非常出色,因此有理由认为每个 GPU chiplet 的时钟频率都可以独立更改。 你明白我要说什么了吗?
如果 macOS 真的很聪明,它应该看到 Anukari 正在运行两个独立 GPU 工作负载:物理 kernel 和自旋 kernel。 为什么不将它们在单独的 GPU chiplet 上运行? 这很聪明。 而且,如果我是对的,GPU chiplet 具有独立的时钟频率,那么 Anukari 就完蛋了,因为愚蠢的自旋工作负载将获得快速时钟 GPU,而音频工作负载将获得慢速时钟 GPU。
我可能错了。 另一种可能性是 GPU 具有单一时钟频率,并且我的自旋工作负载过于保守,无法说服功能更强大的 GPU 提高时钟频率。 也许我的自旋 kernel 可以加热 M1 GPU,但不能加热 M4 Pro GPU,因为 M4 Pro 更快。
你认为 macOS 应该怎么做才能解决这个问题?
Apple 工程师显然比我更了解这一点,但我将提出几个显而易见的可能性。
解决方案 1: 在 macOS 上,音频处理是在一个线程(或一组线程)上完成的,该线程称为 Audio Workgroup。 这些在 Apple 的文档 此处 中进行了解释。 在 Audio Workgroup 中,操作系统了解到线程具有实时约束,并适当地优先处理这些线程。 这实际上是一项了不起的创新,因为在 Audio Workgroup 出现之前,如果不出现优先级反转等问题,实际上不可能在多个线程上安全地进行实时音频处理。
Audio Workgroup 的概念可以扩展到涵盖 GPU 上的处理。 由 Audio Workgroup 线程管理的任何 MTLCommandQueue 都可以被视为实时的,并且可以相应地调整 GPU 时钟。
解决方案 2: Metal API 可以简单地在 MTLCommandQueue 上提供一个选项,以指示它对实时敏感,并且可以相应地调整处理该队列的 GPU chiplet 的时钟。
解决方案 3: 有人可能会指出我是一个白痴,并且已经有某种方法可以获得我想要的东西,而且整个帖子都是在浪费我的时间。 这将是很棒的。
Apple 新的 Game Mode 是否会有所帮助?
Game Mode 似乎与 Anukari 所需的非常相似。 但是,Game Mode 是在进程级别,而 Anukari 主要用作其他进程中的插件,这些进程不支持 Game Mode,而且无论如何 Anukari 无法控制。 此外,Anukari 通常不是全屏的,Game Mode 需要全屏。
Windows 性能如何?
完全没有问题。 我不知道是因为 Windows 让用户可以更多地控制系统的性能状态,还是因为例如 NVIDIA 驱动程序对功耗不太在意,或者什么原因。 但是在 Windows 上不需要自旋循环。
对于 Apple 来说,拥有一台具有相当羸弱 GPU 的 Windows PC 可以正常运行 Anukari,而最昂贵的 Mac M4 Max 却会卡顿,这并不是一件光彩的事,因为显然 Apple 的硬件 令人难以置信 并且只需要稍微释放一下。
为什么不直接对 GPU 代码进行流水线处理,使其饱和 GPU?
对于吞吐量工作负载,这正是我要做的。 但是 Anukari 对延迟敏感,而不是对吞吐量敏感。
流水线的想法可能是 Anukari 可以提前安排多个物理模拟 kernel,以便 GPU 可以同时处理当前的音频样本块,而 CPU 正在为 GPU 准备下一个块。 (这还可以缓解 kernel 调用延迟开销,但这并不重要。)
但是任何了解流水线的人都知道它会以增加延迟为代价来提高吞吐量。 Anukari 实时处理音频,因此每个 kernel 调用都需要访问实时音频输入数据(例如,来自麦克风)。 因此,Anukari 不能做诸如推测执行之类的事情,即提前处理下一个音频块,因为它没有执行所需的输入数据。
为什么不在与物理 kernel 相同的 MTLCommandQueue 中运行自旋 kernel?
此解决方案可能会解决自旋和物理 kernel 最终在单独的 GPU chiplet 上运行的问题(如果这确实是问题)。
实际上,我 已经 尝试过了。 它的不起作用的原因再次是因为 Anukari 对延迟敏感。 发生的事情是,有时自旋 kernel 运行时间过长,从而减少了运行物理 kernel 的时间。 我尝试过使用小型自旋 kernel,使用易失性统一内存来允许 CPU 写入“提前退出 kernel”标志。 即使有了这些恶作剧,有时自旋 kernel 也会减少物理 kernel 的时间。
为什么不直接使 GPU 代码更高效?
因为模拟是 ALU 密集型的,所以从典型的优化唾手可得的成果中获得的性能并不多:改善内存访问模式。 要加快 Anukari 的物理 kernel 的速度,唯一真正有帮助的是优化算术吞吐量。 例如,Anukari 在可能的情况下使用 FP16 数学来更好地饱和 Apple 的 ALU。 指令已使用微基准重新排序。 所有物理状态都在 L1 内存中。 加载被重新排序以进行矢量化。 还有很多。
此外,Anukari 充分利用了 Apple 的 SIMD-group 中的线程(大致)共享一个指令指针的事实。 不同的物理对象具有高度不同的代码分支路径,因此由于在顶级分支路径串行执行时需要指令屏蔽,因此在 SIMD-group 中模拟两种类型的对象很慢。 为了避免这种情况,Anukari 动态优化物理对象的内存布局,以最大限度地减少每个 SIMD-group 中执行的对象类型数量。 此优化在 此处 进行了详细描述,并且是一个巨大的性能提升。
我想说的是,我已经 竭尽全力 从 Apple 的硬件中榨取每一滴性能。 这样做很有趣!
还有一些算术优化可以完成,但它们将是相当微小的。 我们谈论的是个位数的百分点加速。 Anukari 的 GPU 代码已经非常快。 如果您好奇,有关 Anukari 优化的更多信息请参见 此处。
为什么要使用 GPU?
在功能强大的机器上,Anukari 可以模拟 768 - 1024 个物理对象。 每个对象都可以任意连接到其他对象,这意味着它们会相互影响。 每个对象都必须以音频采样率(通常为每秒 48,000 个采样)在隐式 Euler 积分中向前步进。 每个对象都有 3 到 10 个影响其行为的参数。 有些行为涉及昂贵的数学运算,例如矢量旋转、exp()、log() 等。
这在 CPU 上根本不可行! 相信我,我尝试过了。 差得很远。 GPU 只是有大量的 ALU,它让我可以显式控制 L1 缓存布局,并且像 threadgroup_barrier 这样的并发构造允许大规模并行完成物理积分步骤,而不会出现一致性问题,而无需昂贵的 CPU 互斥锁。
我要再说一遍:没有 GPU 处理,Anukari 就不存在。
为什么 Apple 应该关心 Anukari 需要什么?
也许他们不应该,我不知道。 Anukari 是一家小型初创公司。 这是一个小众产品。 它在做一些奇怪的事情。
另一方面,喜欢 Anukari 的人真的很喜欢它。 我认为有史以来最伟大的 DOOM 游戏的作曲家 Mick Gordon 随机出现,并使用 Anukari 制作了一个令人难以置信的 demo,震惊了所有人。 Anukari 受到真正热爱合成器的人(例如 CDM)的好评。 在我没有发起的随机互联网帖子中出现了评论,例如,“这是我过去 10 年里尝试过的最具创意的插件。
但最重要的是,Anukari 正在以我认为非常酷的方式使用 Apple 的硬件,让用户能够做他们以前从未做过的事情。 而 Apple 的硬件完全能够胜任这项任务。 但它只需要朝着正确的方向稍微推动一下。
为什么不使用 GPU Audio 的 API?
我真的不想包含最后一个问题,但这里是为了解决 GPU Audio 的 CEO Alexander Talashov 喜欢浏览有关 Anukari 的帖子并建议如果 Anukari 使用他的 API 就可以解决我们的问题的事实。 如果这是真的,我会很高兴。
Alexander 是个很棒的人,他的产品 (GPU Audio) 也是一款很棒的产品。 我见过他本人,他对使 GPU 可用于 DSP 充满热情。 音频爱好者应该去看看,它真的很酷。 我祝 GPU Audio 取得巨大成功,并支持他们的产品。
但是... GPU Audio 对 Anukari 没有任何有用的东西。 从根本上讲,问题在于 Anukari 与传统的 DSP 应用程序完全不同。 它是一个数值微分方程积分器,与视频游戏物理引擎非常相似,而不是 DSP 应用程序。 当然,物理引擎内部有一些 DSP 位,例如 Anukari 在物理世界中的麦克风确实具有压缩功能,并且这是与 GPU 上的物理计算内联完成的。 但是绝大多数计算是 Eulerian 积分。
需要理解的是,我正在裸 Metal 层(是的,双关语)对 GPU 进行编程,并且正在利用许多硬件功能和荒谬的领域特定优化,这对于使其工作是绝对必要的。 而我所需要的只是 Apple 可靠地提高 GPU 的时钟频率。