29 Nov 2022
大家好,我是朝日丽奈!
marcan 让我写一篇关于 M1 GPU 的文章,所以我们在此见面了~!在过去的几个月里,我们走过了漫长的道路,有许多内容要讲。我希望你们能喜欢!
也许你知道什么是 GPU ,但你知道它们内在的工作机制吗?让我们一起来看看吧!几乎所有的现代 GPU 都有着相同的主要部件:
一组着色器核心 (shader cores) ,通过运行用户定义的程序来处理三角形 (顶点数据 vertex data) 和像素 (片段数据 fragment data)。这些程序在每个 GPU 上都使用了不同的自定义指令集!
栅格化单元 (rasterization units)、纹理采样器 (texture samplers)、渲染输出单元 (render output units) 和其他一些与着色器一起工作的东西,将应用中的三角形转化为屏幕上的像素。具体的工作方式因 GPU 而异!
一个命令处理器 (command processor) ,获取来自应用的绘制命令,将其设置到着色器核心进行处理。这包括了诸如需要绘制哪一组三角形、使用什么全局属性、使用什么纹理、使用什么着色器程序,以及最终图像在内存中的位置等数据。然后它将数据发送给着色器核心以及其他单元,以便操作 GPU 进行实际的渲染。
一个内存管理单元 (memory management unit, MMU) ,负责限制特定应用在使用 GPU 时访问的内存区域。这样不同的应用间才不会相互冲突或干扰。
(这里进行了简化,实际上还有很多部分因 GPU 不同而不同,但这里提到的是最重要的部分!)
为了以合理的、最安全的方式与这些活动部件打交道,现代 GPU 驱动分成了两个部分:用户空间驱动 (user space driver) 和内核驱动 (kernel driver) 。用户空间驱动负责编译着色器程序并将 API 调用 (例如 OpenGL 或 Vulkan) 翻译成一组特定的命令,之后将被命令处理器用来渲染场景。同时,内核驱动负责管理 MMU 和处理不同应用的内存的分配和再分配,并决定以何种方式何时将命令发送给命令处理器。所有现代 GPU 驱动都是如此工作的,在主流操作系统上!
在用户空间驱动和内核驱动之间存在着某种为不同 GPU 系列定制的 API 。这些 API 通常对每个驱动程序都是不同的!在 Linux 我们称之为 UAPI ,但在每个操作系统里都有类似的东西。UAPI 使用户空间驱动得以向内核驱动请求内存的分配和释放以及向 GPU 提交命令。
这意味着如果要让 M1 GPU 能在 Asahi Linux 中工作,我们需要两部分:一个内核驱动和一个用户空间驱动!🚀
回到2021年 Asahi Linux 项目刚启动的时候,Alyssa Rosenzweig 加入到了项目中开始进行对 M1 GPU 的逆向工程。与 Dougall Johnson (一个专注于编写 GPU 着色器架构文档的人) 一起,她开始逆向所有用户空间的东西。这包括着色器和所有为了设置渲染需要的命令组结构。这是一个巨大的工程,而她却在不到一个月的时间里就已经能够绘制出第一个三角形!她真是太了不起了!如果你还不知道她关于剖析 M1 GPU 的系列文章我建议你访问她的网站看一看!✨✨
但是等一下,如果没有内核驱动的配合,她怎么能在用户空间驱动上工作?很简单,她在 macOS 上进行! Alyssa 对 macOS 的 GPU 驱动 UAPI 进行了逆向工程,足以分配内存并向 GPU 提交她自己的命令,这样她就可以在用户空间部分工作而不必担心内核部分。这超级酷!她开始给 Mesa (Linux 用户空间图形技术栈) 编写一个 M1 GPU OpenGL 的驱动。仅仅几个月后,她就已经通过 75% 的 OpenGL ES2 一致性测试,都是在 macOS 上进行的!
今年早些时候,她的工作实在超前,甚至于她可以在一个完全开源的 Mesa OpenGL 技术栈上运行游戏,在苹果的 macOS 的内核驱动上运行!但是我们仍然没有 Linux 的内核驱动……是时候解决这个问题了! ✨
在今年4月份,我决定开始尝试弄清楚如何编写一个 M1 GPU 内核驱动!当我开始工作的时候 Scott Mansell 已经完成了一些侦查工作……并且很显然这不是一个普通的 GPU 。在最初的几个月里,我致力于编写和改进用于 GPU 的 m1n1 hyperisor 跟踪器,然后我发现了一些在 GPU 世界中非常、非常不寻常的东西。
通常情况下,GPU 驱动负责一些细节工作,如调度和决定 GPU 上作业的优先级,并在作业运行时间过长时进行抢占,使应用程序能够公平地使用 GPU 。有时驱动会负责电源管理,有时这由运行在电源管理协处理器上的专门固件完成。有时还会有其他固件负责命令处理的一些细节,但大多数对内核驱动都是不可见的。最后,尤其是对于像 ARM Mali 这样比较简单的"移动式" GPU 来说,让 GPU 渲染东西的实际硬件接口通常非常简单。有一个 MMU ,它像标准的 CPU MMU 或 IOMMU 一样工作,然后命令处理器通常在某种寄存器或环形缓冲区中直接指向用户空间命令缓冲区。因此,内核驱动实际并不需要做太多除管理内存和 GPU 调度之外的事情,而且 Linux 内核的 DRM (直接渲染管理器) 子系统已经提供了大量的助手,使编写驱动变得很容易!虽然仍有一些棘手的问题,如抢占,然而这些并不是让 GPU 在一个全新的驱动中工作的关键。但 M1 GPU 是不同的……
就像 M1 芯片的其他部分一样, GPU 有一个称为 “ASC” 的协处理器,运行着苹果固件并管理 GPU 。这个协处理器是一个完整的 ARM64 CPU ,运行苹果专有的实时操作系统,称为 RTKit ……它负责一切事情!它负责电源管理、命令调度和抢占、故障恢复,甚至是性能计数器、统计数据和温度测量等事项!事实上,macOS 的内核驱动程序根本不与 GPU 硬件进行通信。所有与 GPU 的通信都是通过固件进行的,使用共享内存中的数据结构来告诉它该怎么做。而那里有着很多这样的结构……
初始化数据 用来配置固件中的电源管理设置和其他 GPU 全局配置数据,不知为何还包括了色彩空间转换表?!这些数据结构有近1000个字段,而我们甚至还没有把它们全部搞清楚!
提交管道 用于在GPU上排队工作的环形缓冲区。
设备控制信息 用于控制全局 GPU 操作。
事件消息 当有事件发生时 (如命令完成或失败) ,固件会将其送回驱动程序。
统计数据、固件日志和追踪信息 用于记录 GPU 状态信息和调试。
命令队列 代表了单个应用的待处理的 GPU 工作。
缓冲信息、统计数据和页列表结构 用于管理平铺顶点缓冲区。
上下文结构和其他一些东西 让 GPU 固件跟踪正在发生的事情。
顶点渲染命令 它告诉 GPU 的顶点处理和平铺部分如何处理来自用户空间的命令和着色器,以运行整个渲染通道的顶点部分。
片段渲染命令 它告诉 GPU 的光栅化和片段处理部分如何将顶点处理中的平铺顶点数据渲染到实际的帧缓冲区中。
甚至比这更复杂! 顶点和片段渲染命令实际上是非常复杂的结构,其中有许多嵌套结构,然后每个命令实际上都有一个指向更小的由 GPU 进行解释的命令的"微序列"的指针,就像一个定制的虚拟 CPU !通常这些命令设置渲染通道,等待它完成,并进行清理……但它也支持诸如时间戳命令、甚至是循环和算术的东西! 这真是太疯狂了! 而所有这些结构都需要填入关于将要渲染的内容的密切细节,比如指向深度和模板缓冲区的指针、帧缓冲区的大小、是否启用 MSAA (多重采样抗锯齿) 以及如何配置、指向特定辅助着色器程序的指针等等,以及更多东西!
事实上,GPU 固件与 GPU MMU 有一种奇怪的关系。它使用相同的页表!固件实际上采用了 GPU MMU 使用的相同的页表基指针,并将其配置为其 ARM64 页表。因此,GPU 内存就是固件内存! 这真是太疯狂了! 这里有一个共享的"内核"地址空间 (类似于 Linux 中的内核地址空间) ,由固件本身所使用,也被它与驱动的大部分通信所使用。然后一些缓冲区与 GPU 硬件本身共享,并有"用户空间"地址,这些地址为使用 GPU 的每个应用程序提供单独的地址空间。
那么,我们是否可以将所有这些复杂的东西移到用户空间,并让它设置所有这些顶点/片段渲染命令?不!因为所有这些结构都和固件本身一起在共享的内核地址空间里,而且它们有大量的指针,它们在使用 GPU 的不同进程之间是不隔离的!所以我们不能让应用程序直接访问它们,因为它们可能会破坏彼此的渲染……所以这就是为什么 Alyssa 在 macOS UAPI 中发现了所有这些渲染细节……
由于获得所有这些结构的正确性对于 GPU 的工作和固件的不崩溃至关重要,我需要一种方法,在我进行逆向工程的时候能够快速地对它们进行实验。值得庆幸的是,Asahi Linux 项目已经有了这方面的工具。m1n1 Python 框架! 由于我已经为 m1n1 管理程序写了一个 GPU 跟踪器,并用 Python 填写了结构定义,我决定把它翻过来,使用相同的结构定义开始写一个 Python GPU 内核驱动。Python 很适合做这件事,因为它很容易迭代。更棒的是,它已经可以沟通基本的 RTKit 协议并解析崩溃日志。我改进了这方面的工具,所以我可以看到固件崩溃时到底在做什么。这一切都通过在开发机上运行脚本来完成,开发机通过 USB 连接到 M1 机器上,所以你可以在每次想测试什么的时候轻松地重新启动它,而且测试周期非常快!
起初,驱动的大部分内容实际上只是一堆硬编码的结构,但最终我设法把它们弄对了,并渲染出一个三角形!
不过这只是一个黑进去的演示……在开始做 Linux 内核驱动之前,我想确保我对一切都有足够的了解,以便正确设计驱动。只渲染一帧已经足够容易,但我希望能够渲染多帧,并测试诸如并发和抢占的东西。所以我真的需要一个真正的"内核驱动"……但这在 Python 中是不可能做到的,对吗?!
实际上,Mesa 有一个叫做 drm-shim 的东西,这是一个模拟 Linux DRM 内核接口的库,通过用户空间的一些假处理来代替它。通常情况下,它被用于像着色器 CI 这样的事情,但它也可以用来做更疯狂的事情……所以……如果我在 drm_shim 里面塞进一个 Python 解释器,并从它那里调用我的整个 Python 驱动原型,会怎么样?
我能否在 Mesa 上运行 Inochi2D ,使用 Alyssa 的 Mesa M1 GPU 驱动,在 drm-shim上,运行嵌入的 Python 解释器,在 m1n1 开发框架上向我的 Python 原型驱动发送命令,通过 USB 与真正的 M1 机器通信,来回发送所有数据,以便驱动 GPU 固件和自我渲染?这将是多么荒谬的事情?
更荒谬的是它真的实现了!✨
有了这个恐怖的 Mesa+Python 驱动栈,我开始对最终的内核驱动必须如何工作以及它必须做什么有了更好的认识。它必须要做很多事情!没有办法绕过涉及的100多个数据结构……如果发生任何问题,所有的东西都会坏掉!固件不对任何东西进行完备检查(可能是为了性能),如果它遇到任何损坏的指针或数据,它就会崩溃或盲目地覆盖数据!更糟糕的是,如果固件崩溃了,唯一的恢复方法就是完全重启机器!😱
Linux 内核的 DRM 驱动是用 C 语言编写的,而 C 语言并不是编写复杂数据结构管理的最好语言。我必须手动跟踪每一个 GPU 对象的生命周期,如果我犯了任何错误,可能会导致随机崩溃,甚至是安全漏洞。我该如何战胜这些困难呢?有太多的事情会出错,而 C 语言一点也不能帮助你解决这些问题!
除此之外,我还必须支持多个固件版本,而苹果公司并没有在不同的版本中保持固件结构定义的稳定!作为实验,我已经添加了对第二个版本的支持,我最终不得不对数据结构做了100多处改动。在 Python 演示中,我可以用一些花哨的元编程来使结构字段以版本号为条件……但 C 语言没有这样的东西。你必须使用一些小技巧,比如用不同的 #define
多次编译整个驱动程序!
就在这时有一种新的语言出现了……
大约在同一时间,关于 Rust 即将被 Linux 内核正式采用的传言开始出现了。Rust for Linux 项目几年来一直致力于添加官方支持,而且看起来他们的工作可能很快就会被合并。我可以……我可以用 Rust 编写 GPU 驱动吗?
我对 Rust 没有太多的经验,但就我所了解的来看,它似乎是一种编写 GPU 驱动的更好的语言!我对两件事特别感兴趣:一是它是否能帮助我对 GPU 固件结构的生命周期进行建模 (尽管这些结构与 GPU 指针相连,而从 CPU 的角度来看,并不是真正的指针) ;二是 Rust 宏是否能解决多版本问题。因此,在直接进入内核开发之前,我向 Rust 专家寻求帮助,用简单的用户空间 Rust 制作了一个 GPU 对象模型的玩具原型。Rust 社区非常友好,有几个人帮助我完成了所有的工作。没有你们的帮助,我是不可能做到的! ❤
而且看起来它可以工作! 但是 Rust 仍然没有被 Linux 主线所接受……而且我将进入一个未知的领域,从来没有人做过这样的事情。这将是一场赌博……但当我越考虑这件事,我的内心就越告诉我 Rust 是个好办法。我和 Linux DRM 的维护者以及其他一些人聊过这个问题,他们似乎很热情,或者至少接受了这个想法,所以……
所以我决定去做这件事!
由于这将是第一个 Linux Rust GPU 内核驱动,我有很多工作要做!我不仅要编写驱动本身,还要为 Linux DRM 图形子系统编写 Rust 抽象。虽然 Rust 可以直接调用 C 函数,但这样做并没有 Rust 的任何安全保证。因此,为了从 Rust 中安全地使用 C 代码,首先你必须包装出一个安全的 Rust 式 API。结果我仅是为了抽象就写了将近1500行的代码。而且为了获得一个好的、安全的设计,又花了很多时间进行思考和重写!
8月18日,我开始编写 Rust 驱动。最初,它依靠 C 代码来处理 MMU (部分是从 Panfrost 驱动中复制的) ,尽管后来我决定用 Rust 重写所有的代码。在接下来的几周里,我加入了我之前设计的 Rust GPU 对象系统,然后用 Rust 重新实现了 Python 演示驱动的所有其他部分。
我越是使用 Rust ,就越是爱上了它!它给我的感觉就像是 Rust 的设计引导你走向好的抽象和软件设计。编译器非常挑剔,然而代码一旦编译成功,就会给你信心觉得它能可靠地工作。有时我在让编译器认同我试图使用的设计时遇到困难,然后我就意识到这个设计有根本性的问题!
驱动程序慢慢地成型了。9月24日,我终于让 kmscube 渲染了第一个立方体,用我全新编写的 Rust 驱动程序!
接着,神奇的事情发生了。
仅仅几天后,我就能够运行一个完整的 GNOME 桌面会话!
通常情况下,当你写一个像这样复杂的全新内核驱动程序时,试图从简单的演示应用程序到多个应用程序同时使用 GPU 的完整桌面,结果会引发各种竞赛条件、内存泄漏、使用后释放问题,以及所有的糟糕情况。
但所有这些……都没有发生!我只需要修复一些逻辑错误和内存管理代码核心中的一个问题,然后其他所有的东西都稳定地工作了!Rust 真的很神奇!它的安全特性意味着,只要在少数不安全的部分没有问题,驱动的设计就能保证线程安全和内存安全。它真的能引导你走向不仅是安全而且是好的设计。
当然,代码中总有一些不安全的部分,但由于 Rust 让你从安全的抽象角度考虑问题,所以很容易降低可能出现 bug 的区域的表面积。仍然存在一些安全问题! 例如,我的DRM 内存管理抽象中有一个 bug ,可能导致分配器在所有分配被释放之前就被释放了。但是,由于这些错误是针对某一段代码的,它们往往是显而易见的主要问题(并且可以在代码审查中被审计或捕获),而不是跨越整个驱动的难以捕获的竞态条件或错误用例。通过只需要单独考虑特定的代码模块和安全相关的部分,而不是它们与其他东西的相互作用,你最终将可能的错误数量减少到一个很小的数字。除非你试过 Rust ,否则这很难描述,但它带来了巨大的变化!
哦,还有错误和清理处理! 所有在 C 语言中清理资源的容易出错的 goto
cleanup
风格的错误处理在 Rust 中都消失了。即使只是这样也是值得的。更不用说你还可以得到真正的迭代器,引用计数是自动的!❤
随着内核驱动步入正轨,是时候与 Alyssa 联手开始合作了!她不再受限于只在 macOS 上测试,开始对 Mesa 驱动进行重大改进!我甚至还帮了点小忙^^
我们在 XDC2022 上做了一次联合演讲,当时我们用我们的驱动在 M1 上运行了整个演讲! 从那时起,我们一直在努力为两边增加新的功能,修复错误,并改进性能。我在内核方面增加了对 M1 Pro/Max/Ultra 系列和 M2 的支持,以及更多更好的调试工具和内存分配性能改进。她一直在稳步提高 GL 的一致性,OpenGL ES 2.0 的一致性几乎已经完成,3.0 的一致性超过96%!她还增加了许多新的功能和性能改进,今天你可以在 4K 下玩像 Xonotic 和 Quake 这样的游戏!
由于 GPU 的电源管理是由固件处理的,所有这些都能正常工作。我在 GNOME 会话中以 1080p 测试了 Xonotic ,预计电池运行时间超过8小时! 🚀
对 Vulkan 的支持情况如何?别担心…… Ella 正在努力解决这个问题! ✨✨
前面还有很长的路要走! 我们现在使用的 UAPI 仍然是一个原型,有很多新的功能需要添加或重新设计,以便在未来支持一个完整的 Vulkan 驱动。由于 Linux 规定 UAPI 需要保持稳定,并在不同的版本中向后兼容 (与 macOS 不同),这意味着内核驱动在许多个月内不会向上游发展,直到我们对 GPU 渲染参数有了更全面的了解,并实现了 Vulkan 所需的所有新设计功能。目前的 UAPI 也有性能限制……它甚至还不能在 CPU 处理的同时运行 GPU 渲染!
当然,在用户空间方面还有很多工作要做,改善一致性和性能,并增加对更多 GL 扩展和功能的支持!一些功能,如镶嵌和几何着色器的实现非常棘手 (因为它们需要部分或完全仿真),所以在相当长的一段时间内不要指望完全的 OpenGL 3.2+ 。
但是,即使有这些限制,今天的驱动程序也可以稳定地运行桌面,而且性能每周都在提高!Wayland 现在在这些机器上运行得非常流畅,就像原生的 macOS 桌面一样! 几天前我对显示驱动做了一些改进,Xorg 也运行得很好,尽管由于 Xorg 的设计限制,你可以预料到会出现撕裂和 vsync 问题。Wayland 确实是新平台上的未来! 💫
那么你可以从哪里获得它呢?我们还没到那一步呢! 现在,驱动程序栈的构建和安装很复杂 (你需要定制 m1n1 、内核和 mesa 的构建) ,所以请再等一等吧 我们还有一些问题需要解决……但我们希望在年底前,可以把它作为一个可选择的测试版本带到 Asahi Linux 上! ✨✨
如果你对关注我在 GPU 上的工作感兴趣,你可以关注我 @[email protected] 或订阅我的 YouTube频道 !明天,我将全力弄清 M1 Pro/Max/Ultra 和 M2 的功耗计算,我希望能在那里见到你! ✨
如果你想支持我的工作,你可以在 GitHub 上向 marcan 的 Asahi Linux 进行资金捐赠 Github Sponsors 或 Patreon,这将有助于我的工作! 如果你期待一个 Vulkan 驱动,请查看 Ella 的 GitHub Sponsors 页面! Alyssa 自己不接受捐款,但她希望你能捐给像软件自由保护协会这样的慈善机构。(虽然也许有一天我会说服她让我给她买一个M2…^^;;)
本文的翻译和转载均已经过原作者 Asahi Lina 的许可原文地址:https://asahilinux.org/2022/11/tales-of-the-m1-gpu/