M1 GPU 的神話:編寫自己的核心驅動程序

【CSDN 編者按】這是一個基於 Rust 來嘗試編寫 Linux GPU 的核心驅動程序,本文分享了研發的心路歷程!

原文連結:https://asahilinux.org/2022/11/tales-of-the-m1-gpu/

作者 | Asahi Lina

譯者 | 彎月

什麼是 GPU?

什麼是 GPU?

你可能知道 GPU 是什麼,但你了解它們的底層邏輯嗎?幾乎所有現代 GPU 都擁有以下幾個主要元件:

● 若干著色器核心(shader core):通過運行使用者定義的程序來處理三角形(頂點資料)和像素(片段資料)。每個 GPU 都有一套自定義的指令集。

● 光柵化單元、紋理取樣器、渲染輸出單元以及其他元件:這些元件與著色器協同工作,將應用程序中的三角形轉換為螢幕上的像素。具體的工作方式因 GPU 而異。

● 命令處理器:從應用程序獲取繪圖命令,並設置著色器核心來處理。其中包括一系列資料,比如三角形列表、全局屬性、紋理、著色器程序以及保存最終圖像的記憶體位置等等。然後,將這些資料發送到著色器核心和其他單元,並指示GPU完成實際的渲染。

● 記憶體管理單元:使用 GPU 的應用程序都有各自的記憶體區域,該元件的作用就是限制對這些記憶體的訪問,以防止各個應用程序崩潰或相互干擾。

為了以合理、安全的方式組織這些部件,現代 GPU 驅動程序主要分為兩大部分:使用者空間驅動程序和核心驅動程序。使用者空間部分負責編譯著色器程序,並將 API 調用(如 OpenGL 或 Vulkan)轉換為特定的命令列表,供命令處理器渲染場景。而核心部分則負責管理記憶體管理單元, 並處理不同應用程序的記憶體分配和釋放,以及何時、通過何種方式將命令發送到命令處理器。所有現代 GPU 驅動程序在所有主流作業系統上的工作方式都是如此。

使用者空間驅動程序和核心驅動程序之間有一些由GPU自己定義的API。通常每個驅動程序使用的 API 都是不同的。在 Linux 中,我們稱之為 UAPI,但每個作業系統都有類似的 API。使用者空間可以通過這個 UAPI 向核心請求分配或釋放記憶體,並將命令列表提交給 GPU。

這意味著,如果想在 Linux 中使用 M1 GPU,我們需要兩個程序:一個核心驅動程序和一個使用者空間驅動程序。

使用者空間驅動程序的逆向工程

使用者空間驅動程序的逆向工程

2021 年,我們開始對 M1 GPU 實施逆向工程,並與 Dougall Johnson(專門負責記錄 GPU 著色器架構)合作,對所有使用者空間位進行了逆向工程,包括著色器和渲染所需的所有命令列表結構。這是一項繁重的工作,但我們用了不到一個月的時間就畫出了第一個三角形。

但是,如何在沒有核心驅動程序的情況下,使用使用者空間驅動程序的呢?很簡單,使用 macOS。首先,對 macOS 的 GPU 驅動程序 UAPI 進行逆向工程,分配記憶體,並將命令提交給 GPU,這樣即便沒有核心驅動程序,使用者空間驅動程序也可以正常工作。接著,為 Linux 使用者空間圖形棧 Mesa 編寫 M1 GPU OpenGL 驅動程序,僅僅幾個月後,我們就通過了 75% 的 OpenGL ES 2 一致性測試,所有這些工作都是在 macOS 上完成的。

今年早些時候,我們一路領先,在開源 Mesa OpenGL 棧(運行在 macOS 的蘋果核心驅動程序之上)上運行遊戲。下面,我們來解決 Linux 核心驅動程序的問題。

神秘的GPU韌體

神秘的GPU韌體

今年4月,我決定開始琢磨如何編寫M1 GPU核心驅動。在最初的幾個月裡,我全身心投入為 GPU 編寫和改進 m1n1 管理程序跟蹤器,而且我發現了在 GPU 的世界裡非常不尋常的一件事。

通常,GPU 驅動程序會負責一些細節,例如安排和調整任務的優先級,以及在某些作業運行時間過長時搶過主動權,以允許應用程序公平地使用 GPU。有時電源管理由驅動程序負責,而有時則由運行在電源管理協處理器上的專用韌體負責。有時,還有其他韌體負責命令處理的一些細節,但一般核心驅動程序都不知道這些韌體的存在。最後,特別是對於像 ARM Mali 這類更簡單的「移動式」GPU,驅動 GPU 完成渲染工作的硬體接口通常非常簡單,比如 MMU(工作方式與 CPU MMU 或 IOMMU 類似),然後由命令處理器直接獲取指向使用者空間命令緩衝區的指針(通常儲存在某種暫存器或環形緩衝區內)。因此,核心驅動程序除了管理記憶體和安排 GPU 的工作外,實際需要負責的工作並不多, Linux 核心 DRM(Direct Rendering Manager,直接渲染管理器)子系統已經提供了大量幫助程序,因此編寫驅動程序非常容易。雖然有一些棘手的問題,比如搶佔,但這些問題對 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(Multisample anti-aliasing,多重取樣抗鋸齒)及其配置方式、指向特定的輔助著色器程序,以及其他等等。

事實上,GPU 韌體與 GPU MMU 的關係很奇怪。二者使用了同一個頁表。韌體會直接使用 GPU MMU 的頁表基址指針,並將其配置為 ARM64 的頁表。所以,GPU 記憶體就是韌體的記憶體。韌體自身以及與驅動程序的大部分通訊都使用了一個共享的「核心」地址空間(類似於 Linux 中的核心地址空間),而一些緩衝區是與 GPU 硬體共享的並具有「 使用者空間」地址,這些地址在每個使用 GPU 的應用程序中也能夠有單獨的地址空間。

那麼,我們能否將所有這些複雜性轉移到使用者空間,並讓它設置所有頂點或片段的渲染命令?不行!由於所有這些結構與韌體本身都位於共享的核心地址空間中,並且它們之間有大量指針,因此在使用 GPU 的不同進程之間並不是獨立的。所以,我們不能讓應用程序直接訪問它們,因為它們有可能會破壞彼此的渲染。這就是我們能在 macOS UAPI 中找到了所有這些渲染細節的原因。

使用 Python 編寫GPU 驅動程序

由於正確設置所有這些結構關係到 GPU 與韌體是否會崩潰,因此我需要一種在逆向工程時快速試驗它們的方法。值得慶幸的是,Asahi Linux 項目有一個款工具:m1n1 Python 框架。因為我已經在為 m1n1 管理程序編寫 GPU 跟蹤器,並用 Python 編寫結構定義,所以我決定使用 Python 編寫 GPU 的核心驅動程序,使用相同的結構定義。Python 非常適合這項任務,因為我可以使用 Python 進行快速迭代開發。另外,Python 可以使用基本的 RTKit 協議通訊,並解析崩潰日誌,我為此改進了工具,這樣在韌體崩潰時就可以看到韌體的行為。所有這些工作都是在開發機器上運行腳本完成的,我的開發機器通過 USB 連接到了 M1 機器上,因此每次測試時,只需要重啟開發機器即可,而且測試周期非常快。

起初,驅動程序的大部分實際上只是一堆硬編碼的結構,但最終我成功地渲染了一個三角形。

不過,這只是一個七拼八湊的演示。我只是想在動手編寫 Linux 核心驅動程序之前,確保自己真正理解內部機制,以確保能夠正確設計驅動程序。雖然只渲染一幀非常簡單,但我希望能夠渲染多幀,並測試一下併發和搶佔等。所以,我所需要的是一個真正的「核心驅動程序」。但這真的可以用 Python 實現嗎?

事實證明,Mesa 有一個名叫 drm-shim 的工具,可以模擬 Linux DRM 核心接口,並在使用者空間中使用一些假的接口替換掉它們。通常,我們用這個庫來處理著色器 CI 等,但我們也可以用它來做一些更瘋狂的處理。

我是否可以這樣做:讓 Inochi2D 在 Mesa 上運行,後者是運行在 drm-shim 之上的 M1 GPU 驅動程序,而 drm-shim 運行在一個嵌入式 Python 直譯器上,將命令發給在 m1n1 開發框架上運行的 Python 原型驅動程序,後者再通過 USB 與真正的 M1 機器通訊並收發資料,從而驅動 GPU 韌體進行渲染?聽起來不太靠譜?

然而,這真的可行!

然而,這真的可行!
編寫 Linux 核心的新語言

編寫 Linux 核心的新語言

由於我的 Mesa+Python 驅動程序真的可以運行,我開始更好地了解核心驅動程序的內部機制以及必須實現的功能。事實證明,核心驅動程序需要完成的任務很多。首先,我必須同時兼顧 100 多個資料結構,一旦出現任何問題,一切都會崩潰。韌體不會做任何檢查(可能是為了性能),一旦遇到錯誤的指針或資料,它就會崩潰或或盲目地覆蓋資料。更糟糕的是,如果韌體崩潰,唯一的恢復方法就是重啟機器。

Linux 核心 DRM 驅動程序是用 C 編寫的,但 C 不是編寫複雜的資料結構管理的最佳語言。我必須手動跟蹤每個 GPU 對象的生命週期,一旦發生任何錯誤,都有可能導致崩潰甚至安全漏洞。我要怎樣才能做到這一點?可能出錯的地方太多了,C 語言根本幫不了我。

最重要的是,我必須支持多個版本的韌體,蘋果的韌體結構定義在不同版本之間並不一致。作為實驗,我添加了對第二個版本的支持,最終被迫修改了 100 多次資料結構。在 Python 演示中,我可以通過一些元程式設計來實現,根據版本號來構建不同的結構欄位,但 C 語言中沒有類似的功能。我必須使用一些技巧,例如使用不同的 #define 多次編譯整個驅動程序。

我必須尋找一種新語言……

大約在同一時間,關於 Rust 很快被 Linux 核心正式採用的傳言開始出現。多年來,Rust for Linux 項目一直致力實現這種支持,看起來他們的努力即將有成果。我可以用 Rust 編寫 GPU 驅動程序嗎?

我沒有太多使用 Rust 的經驗,但根據我的了解,這種語言很適合編寫 GPU 驅動程序。我對兩個問題特別感興趣:Rust 是否可以幫助我模擬 GPU 韌體結構的生命週期(即使這些結構與 GPU 指針相關聯,從 CPU 的角度來看這算不上真正的指針),Rust 宏是否可以處理好多個版本的問題。因此,在開始核心開發之前,我向 Rust 專家尋求幫助,並在簡單的使用者空間 Rust 中製作了 GPU 對象模型的一個原型。Rust 社區非常友好,有幾個人幫助我完成了所有工作。在此表示感謝!

看起來,選擇 Rust 似乎可能。但是,Rust 尚未被主流 Linux 接受,這意味著我即將進入一個未知的領域。這將是一場賭博。猶豫再三,我內心一直有個聲音告訴我,Rust 是正確的選擇。我與 Linux DRM 的維護人員就此進行了交談,他們似乎也接受了這個想法,所以,我決定試試看。

使用 Rust 編寫 GPU 核心驅動程序

由於這將是第一個使用 Rust 編寫的 Linux 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 的設計可以引導你設計出更好的抽象和軟體。Rust 的編譯器非常苛刻,但程式碼一旦通過編譯,你就可以相信它能可靠地工作。有時,我很難讓編譯器滿意我嘗試使用的設計,隨後我就會意識到我的設計存在問題。

逐漸地,我的驅動程序有了眉目。9 月 24 日,我終於使用我全新的 Rust 驅動程序渲染了第一個立方體。

更不可思議的是,幾天後,我就可以運行完整的 GNOME 桌面會話了。

Rust 很神奇

Rust 很神奇

一般來講,編寫這樣的一個複雜的核心驅動程序,想從簡單的演示應用程序發展到支持整個桌面、多個應用程序併發使用GPU的系統,會引發很多競爭條件、記憶體洩漏、使用後釋放記憶體的問題,以及其他各種問題。

但這一切問題都沒有發生。我只修復了一些邏輯錯誤和記憶體管理程式碼核心的一個問題,而其他一切都可以穩定運行。Rust 真的很神奇。它的安全特性可以保證驅動程序的執行緒與記憶體安全,並引導我們實現安全且良好的設計。

當然,程式碼中總是存在一些不安全的因素,但是由於 Rust 會迫使你從安全抽象的角度進行思考,因此出現 bug 的可能性也保持在很低的水平。但是,有些安全問題仍然無法避免。例如,我的 DRM 記憶體管理抽象中存在一個 bug,最終可能會導致在所有分配的記憶體被釋放之前,分配程序本身先被釋放。但是由於這類錯誤僅限於特定的程式碼,因此往往是很容易發現的主要問題(可以通過程式碼審查發現)。因此,你只需要單獨考慮特定的程式碼模組以及與安全相關的部分,而不必擔心它們與其他所有內容的互動,最終你需要擔心的錯誤數量也會很少。Rust 真的很神奇,若非親身嘗試,否則很難形容。

另外,還有錯誤和清理。在C語言中,我們需要通過 goto cleanup 風格的錯誤處理來清理資源,這些處理很容易出錯,但 Rust 沒有這個問題。僅此一點,Rust 就值得嘗試。更不用說,自動化的迭代器和引用計數等等。

相關文章

Rust vs Go,到底該怎麼選?

Rust vs Go,到底該怎麼選?

【CSDN 編者按】擁有 40 多年程式設計經驗的知名 Go 開發者與作家 John Arundel 在其個人部落格分享了《Rust vs ...

「聽我說,創業公司選擇 Rust 需謹慎」

「聽我說,創業公司選擇 Rust 需謹慎」

摘要:近年來,Rust 絕對是一門成長速度飛快的程式語言,許多國內外大廠都開始關注這門年輕的語言,但本文作者表示,對於創業公司而言,Rust...

CNNVD通報Oracle多個安全漏洞

CNNVD通報Oracle多個安全漏洞

近日,CNNVD通報Oracle多個安全漏洞,其中Oracle產品本身漏洞60個,影響到Oracle產品的其他廠商漏洞247個。包括Orac...

我用 Rust 程式設計的這兩年

我用 Rust 程式設計的這兩年

摘要:近年來,Rust 被越來越多大廠投入使用,如微軟的 VS Code、Visual Studio 等工具已提供對 Rust 的良好支持,...

C、C++ 將退休,Rust 欲上位?

C、C++ 將退休,Rust 欲上位?

整理 | 蘇宓 Rust 這把火在微軟Azure CTO Mark Russinovich的助力下,似乎越燒越旺。而每當波及程式語言時,紛爭...