M4 Mac Mini用Linux GPUドライバーを1ヶ月で構築する
Building a Linux GPU Driver for the M4 Mac Mini in One Month (codyho.dev)
要約
Niklas氏とCody Ho氏は、わずか1ヶ月でM4 Mac MiniおよびMacBook Neo向けのOpenGL ES 3.0準拠GPUドライバーを開発しました。このプロジェクトでは、AppleのAGX GPUの複雑なファームウェアABIとユーザー空間コンポーネントのリバースエンジニアリングが必要でしたが、クリーンルーム手法を用いて検証可能な形で実施されました。開発されたドライバーは、ChromeやFirefoxでのWebGL表示、さらにはMinecraftを200fpsで実行できる性能を示しています。
全文翻訳
I Came, I Prompted, I Left Part 2: 1ヶ月でゼロからGPUドライバーを構築する
前回のブログ記事: https://codyho.dev/blog/hypervisor-macbook-neo/
何をしたか
要約: Niklas と私は、通常数年かかるプロセスを約1ヶ月で完了させ、M4 Mac Mini および MacBook Neo 向けの完全な OpenGL ES 3.0 準拠 GPU ドライバーを構築しました。以下は、M4 Mac Mini 上で動作する Chrome および Firefox の WebGL の様子で、コンポジットも機能しています。
[画像: 私たちのドライバー上で three.js WebGL デモを実行する Chrome および Firefox。]
最も重要なのは、このドライバーは Minecraft を 200fps で実行できるほど高速であることです。
[画像: M4 Mac Mini 上で 212fps で実行される Minecraft。]
このドライバーの構築には、AGX(Apple の GPU の名称)の信じられないほど複雑なファームウェア ABI およびユーザー空間コンポーネントのリバースエンジニアリングが含まれていました。これらはすべて、確立された手法を用いた、透明で検証可能なクリーンルーム方式で行われました。コードはまだエンドユーザー向けではありませんが、可能な限り早く提供できるよう努めています。
どうやってやったか
以前、macOS をリバースエンジニアリングするためのハイパーバイザーを構築しました。次に、それを使って実際に何か有用なことをするという目標が生まれ、GPU ドライバーの作成ほど良いターゲットはありませんでした。GPU は、現代のシステムにとって事実上必須であり、そうでなければすべて CPU レンダリングになり、桁違いに遅く、電力効率も悪くなります。私たちの目標は、M4 Mac Mini および MacBook Neo 向けの準拠した OpenGL(そして間もなく Vulkan)ドライバーを実装することでした。
通常、GPU ドライバーの構築は数年かかる作業ですが、私たちの目標は数日でそれを達成することでした。結果として、数日というのは楽観的すぎましたが、数週間でも依然として大幅な進歩です。この数週間で、私たちは以下を行いました。
- M4、A18 Pro、および(主に)M5 のユーザー空間を、ライブプロービングのみを使用してリバースエンジニアリングし、Apple のドライバーが出力しないハードウェアサポート機能および命令を発見しました。
- 新しいカスタム IR/シェーダーコンパイラ、コマンドストリームビルダー、およびその他の多くのコンポーネントを含む、完全に機能するユーザー空間ドライバーを構築しました。
- 以前構築したハイパーバイザーからのトレースを使用して、AGX ファームウェア ABI 全体をゼロからリバースエンジニアリングしました。
- 前述のファームウェア ABI 用の完全な Linux カーネルドライバーを実装しました。
このプロセス全体を通して、Apple のバイナリは一切見ておらず、ハードウェアトレース(ハイパーバイザーから取得)と自作のシェーダーのみを使用しました。グラフィックスのユーザー空間 RE に関しては、必要な Apple のブロブを不透明なオブジェクトとして扱うよう注意しました。友人にこれらのブロブに関するドキュメントを書いてもらい 1、クリーンルーム実装を自身で行えるようにしました(これは主に、何かを試してそれが機能するまで盲目的に続けることで構築されました)。すべての実験結果を公開しており、誰でも私たちの作業の出所を検証できます(成果物の下にあるツイン agx-re リポジトリを参照してください)。
このブログ記事は、ユーザー空間とカーネル空間の2つのパートに分かれています。これは、すべてのモダン GPU ドライバーの分割を反映しています。カーネルは、ファームウェアとのインターフェース、バッファの割り当て、スケジューリングの管理を担当し、それらのバッファの実際のコンテンツとスケジューリングされているものは不透明です。ユーザー空間は、GPU が実際にどのように機能するかを理解し、それらのバッファをデータで満たす責任があります。
カーネル空間
Apple Silicon では、カーネルドライバーはハードウェアと直接インターフェースしません。代わりに、RTKit というカスタム RTOS を実行している GPU ファームウェアと通信します。つまり、カーネルドライバーの最初のステップはハードウェアとの通信ではなく、ファームウェア ABI を理解することです。
ファームウェア ABI は、このプロジェクトで最も厄介な部分でした。なぜなら、合理的な ABI ときれいなインターフェースを考案するという賢明なことをする代わりに、Apple は基本的に通常のカーネルドライバーを取り、それを半分にカットし、半分を AGX に入れ、それをファームウェアと呼び、カーネルドライバーのもう半分がメモリ内の共有構造体を使用して通信するようにしたからです。これらの構造体の多くには、ファームウェアが所有するフィールド(決して変更してはならず、リバースエンジニアリングから学ぶ必要がある)と、ホストが制御するフィールドが混在しています。ABI の複雑さのアイデアを得るために、これが M1/M2 の共有メモリツリーの外観です。
[画像: M1/M2 ファームウェア ABI]
Asahi Lina は、M1/M2 カーネルドライバーを構築するために、このすべてを厳しい12時間の日々で解明しました。これは驚くべき技術的偉業です。残念ながら、A18 Pro ファームウェア ABI(私は MacBook Neo で RE 作業を開始し、後に M4 Mac Mini にピボットしました)は、すでに非常に複雑な M1 ファームウェア ABI よりも著しく複雑です。
[画像: A18 Pro ファームウェア ABI]
一体どうしたんだ、Apple。 A18 が持つものを確認してください。
- 構造体が 1.5 倍多い
- ポインターが2倍多い
- 作業の送信プロセスが著しく複雑化
RE プロセスに摩擦を加える他の多くの問題があります 2。ファームウェア ABI に関するドキュメントはありましたが、非常に不完全で、正直言ってあまり役に立ちませんでした 3。
私のアプローチはシンプルで、M1/M2 マシンを正常にリバースエンジニアリングするために使用されたアプローチに基づいています。macOS が何をしたかを見て、それを再生し、それから自分たちで試すというものです。これはハイパーバイザーによって可能になります。
このアプローチを LLM に説明したところ、それは再生を非常に文字通りに受け取りました。最初に行ったことは、最初のファームウェア可視イベント(「キック」と呼ばれる)を待つことでした。次に、GPU メモリ状態全体のコピーを保存しました。再起動後、保存されたメモリ状態をホストメモリに直接コピーし、キックを実行して、出力ページが変更されるのを確認しました。その後、コード内でこれらのオブジェクトを再構築しようとし、すべてのポインターをたどり、内容を理解しました。 successive な実験を通じて、Codex はコピーするページ数を、リプレイする状態がなくなるまで減らし、すべてソースから構築されました 4。驚くべきことに、Codex は、いつハードウェアをさらにプローブすべきか、いつハイパーバイザーを実行して状態自体をキャプチャすべきかについて、良い判断をしていました。
ホスト作業のクリーンなキャプチャを取得できなかったことが原因で、3つの主要な問題が発生しました。
最初の問題は、GPU ファームウェアが開始した後に送信されたレンダリング作業でした。ファームウェアが開始する前に作業を事前ステージングし、GPU を開始すれば、その作業は期待どおりに完了しましたが、ファームウェアが開始すると、送信された作業は ACK されるだけで、実際には何も行われずに完了しました。ファームウェアが開始すると、状態のキャプチャははるかに困難になります。なぜなら、すべてが動的になり、ファームウェアは簡単にリプレイできない状態を持つステートフルオブジェクトになるからです。
この時点で私が介入し、Codex のプロセスを調べる必要がありました。それは、AGX のライフサイクルの非常に遅い段階でのキャプチャをリプレイしようとしていたことが判明しました。そこにはすでに多くの以前のイベントがありました。AGX ライフサイクルのかなり早い段階でのキャプチャを選択するように指示したところ、ファームウェア開始後の最初のキャプチャで、Codex はほぼ即座に問題を発見することができました(単一バイトのディスクリプタが欠落していました)。これには数日かかりました。
2番目の、そして唯一の主要なブロックとなった問題は、コンピューティングでした。AGX は、大まかに言って、コンピューティングとレンダリングの2種類の作業をサポートしています。通常の GUI パスでは、コンピューティング作業は、かなりの量のレンダリング作業がすでに実行された後にのみスケジューリングされます。そのため、コンピューティングワークロードのクリーンなキャプチャを取得するのに長い時間がかかり、Codex が最終的に取得したとき、それは 336MB でリプレイ不可能でした(長時間試みましたが、失敗しました)。また、キャプチャを見てオブジェクト自体を構築しようとし、1週間以上費やしましたが、最終的には成功しませんでした。あまりにも多くの無意味な情報がありました。私の側の問題によって悪化しました。レンダリングが機能するようになった後、コンピューティング作業の送信はもっと簡単になると予想していましたが(ファームウェア ABI は確かにシンプルなので、これは正しかった)、謙虚さを失い、数時間しかかからない簡単なものだと考えました。そのため、LLM のためにタスクを適切にスキャフォールディングしませんでした。
修正は、別の Codex セッションで提供されました。本質的には次のとおりです。
- シングルユーザーモードで起動して GUI を無効にする。これにより、レンダリング作業は行われません。
- Metal(Apple の独自のグラフィックスフレームワーク)が利用可能になった直後に実行されるように、LaunchDaemon をインストールします。
- 提供した小さな Metal プログラムを実行します。
- この小さく純粋なコンピューティングトレースをキャプチャしてリプレイします。
トレースは正常にキャプチャされました。数時間以内に Codex はそれを分解し、数日以内にコンピューティングが機能するようになりました。元のコンピューティングコードベースが機能しなかった理由については… Codex は全く理解していません。機能しているものと機能していないものは非常によく似ています。
hindsight で言えば、最初からこの戦略を取るべきでした。最小限のキャプチャで、結果を乱さないようにシングルユーザーモードで実行することです。私は最終的な問題のために、自分の間違いから学びました。
部分的なレンダリングは、最も理解するのが難しいことの1つになりました。これは、タイルド頂点バッファ(TVB)が現在のジオメトリを格納するのに十分な大きさでない場合(つまり、描画する三角形が多すぎる場合)に発生します。これらの場合、ドライバーがサポートする必要がある2つのオプションがあります。TVB のサイズを増やすか、部分的なレンダリングを実行します。つまり、ジオメトリの一部をレンダリングし、バッファを残りの三角形で再ロードして、部分的なレンダリングを完了します。これらの部分的なレンダリングは、GPU ドライバーに保存と再開を追加することを意味するため、他の作業よりもはるかに、はるかにデリケートであることが判明しました。
以前発見したワークフローは、ここで非常に役立ちました。Codex は1つの部分的なレンダリングトランザクションをリプレイでき、その後、Metal シェーダーを変更して複数の部分的なレンダリングを実行しました(これは、単一のタイルに数千の三角形を叩き込んで部分的なレンダリングをトリガーすることで非常に簡単です)。そして、これらのリプレイ方法を学びました。Codex が正常なリプレイを完了すると、それを自身で構築する方法を学ぶのは時間の問題でした。
カーネルドライバーの構築
Python プロトタイプのドライバーから完全な Linux ドライバーへの移行は3日かかりました。そのうち1日はほぼ完全に無駄になりました。Codex が、理由不明ですが、コンピューティング(最も簡単なタスク)を最初に行うのではなく、部分的なレンダリング(最も難しいタスク)から取り組むことを選択したからです。コンピューティングを最初に行うように指示すると、すべてがスムーズに進みました。
全体的なプロセスは、基本的に次のとおりでした。
- 既存の
drm-shimを Rust で、全く同じパターンに従って書き直します。これにより、同期 Rust ドライバーが得られます。 - フロントエンドを非同期に書き直します。実際の GPU 送信は同期のままです。
- GPU 送信を非同期にリファクタリングし、ポーリングの代わりに、ファームウェアイベントをリッスンし、作業をフェンスに関連付けます。
- バッチ処理された作業送信など、いくつかのローエンドの最適化を実装します。
これらはすべて、LLM が間違いなく実行できる、非常にルーチン的なエンジニアリング作業です。
私が発見した唯一注目すべき点は、Codex がコードが機能しなかった理由をデバッグするためにハイパーバイザーを積極的に使用したことです。これには、フルアドレス空間をキャプチャして既知の良好なサンプルと比較することが含まれます。この種の体系的なデバッグは、Codex が私の好きなコーディングエージェントである理由です。
ユーザー空間
A18 Pro のユーザー空間は M1/M2 とは大きく異なります。新しいディスクリプタ形式、新しい ISA、その他多くの新しいものがあります。Metal を実行するように設計されたタイルベースの遅延レンダラーであること以外は、単に異なる GPU です。
良いニュースは、ユーザー空間 RE には非常に明確なプロセスがあることです。単純に小さな Metal プログラムを書き、コンパイルし、実行し、何が変わったかを確認し、それを分解して、すべてのビットをいじり回して、それらが何をするかを理解します。これが LLM が非常に得意とする、退屈で反復的で定型的な作業のように聞こえるなら、それは正しいでしょう。
RE 作業は2つのフェーズで行われました。最初のフェーズでは、Claude に見つけられる可能性のあるすべての Metal プログラムを見てもらい、ディスアセンブラ、アセンブラを構築し、GPU ドライバーに必要な他のすべてのディスクリプタ/コマンドストリームなどの形式を理解しようとしました。Claude に、完全性のために Linux が使用できないもの(テッセレーションなど)も含めて、すべてを列挙させました。これは成功しましたが、Claude がプログラムをディスアセンブルして再アセンブルできたからといって、自分でプログラムを構築する方法を知っていたわけではありません。このギャップを埋めようとしたとき、つまり、任意のプログラムを実際にコンパイルできるようにすべての命令を理解しようとしたとき、Claude はひどい仕事をし、ほとんど進歩しませんでした。
この時点で、Niklas は M4 Mac Mini 用の drm-shim を完成させ、ユーザー空間 RE の第2フェーズに参加しました。ユーザー空間ドライバーの完成に向けて、私たちは2つの異なるアプローチを取りました。
私のアプローチは、ハードウェア RE を優先し、ハードウェアとすべての命令がどのように機能するかを理解することに焦点を当てることでした。その後、仕様を書き、LLM に実装させ、完全な OpenGL および Vulkan 準拠で終わることを期待していました。これは、LLM の時間のほとんどが、実際に Mesa コードを実装するのではなく、ハードウェアでの実験を書くことに費やされたことを意味します。アイデアは、ハードウェアを理解すれば、すべてが続くというものでした。
Niklas は異なるアプローチを取りました。私はそれを「Mesa ファースト」と表現します。基本的に、彼はまず Mesa を構築しようとし、機能の一部を構築するためにのみ RE を行いました。彼の時間は、Mesa の構築とテスト、および RE の実行に費やされました。
Niklas の方が私よりも著しく速い進歩を遂げたことが判明しました。なぜなら、私のエージェントは、完全性の名の下に、些細で重要でないタスクに多くの時間を費やしていたからです。対照的に、彼のエージェントは実際に Mesa を構築する必要性によって根拠づけられていたため、時間とリソースをより効果的に使用しました。彼は私よりもはるかに速く動いたため、彼がまだ理解していない動作を調査することで、彼の作業をサポートしようとしました。
これは、私が気付いた Codex の大きな制限の1つでした。私が思いつく最良の言葉は「pedantic」(厳格すぎる)です。それは常に非常に徹底的であり、一部のシナリオでは大きな利点となりますが、他の times には全体的な目標を損なうようなランダムな脱線に深く入り込んでしまいます。
RE 中に、ハードウェアはサポートしていましたが、Metal はサポートしていない動作を発見しました。これは、さまざまな命令のビットを直接操作し、Alyssa Rosenzweig が M1/M2 を RE したときと同様に、存在したものを知っていることから存在する可能性のあるものを推測することによって発見されました。これには以下が含まれます。
- ネイティブな単一命令 64 ビット加算
- 128x までの異方性(Metal は 16x でキャップされます)
- 行列ユニットの新しいモード
uniform_movの 7 ビット即値サポート
Mesa の開発
ユーザー空間グラフィックスを構築する上で、私たちのために大きく有利に働くことがいくつかあります。最も注目すべきは、Khronos 互換性テストスイート(CTS)が、私たちのドライバーがパスしなければならない網羅的なテストコーパスであることです。言い換えれば、LLM との作業で最も困難でデリケートな部分、つまり LLM に良いテストを提供して根拠づけることは、すでに私たちにとって完了しています。
さらに、Mesa には私たちの生活を大幅に楽にする優れた抽象化があります。これが Linux 上のモダンな OpenGL スタックの外観です。
[画像: モダンな Linux グラフィックススタックの図。]
私たちは、Gallium、Mesa の内部 API、および AGX ハードウェアセマンティクス間の翻訳を行うだけで済みます。このプロセスの最も重要な部分の1つは、Mesa の内部 IR であり、LLVM IR に非常に似ている NIR から AGX の独自の ISA への翻訳です。ボーナスとして、このコンパイラは将来の Vulkan ドライバーにも再利用できます。
Niklas は、ユーザー空間を RE しながら、ゆっくりと OpenGL の機能を反復処理し、最終的に完全な OpenGL ES 3.0 準拠を達成しました(サポートされていないテストはオプションの拡張機能です)。
[画像: 私たちのドライバー上で合格した OpenGL ES 3.0 準拠テストスイート。]
プロセス全体を通して、既存の M1/M2 の作業から恩恵を受けました。ハードウェアセマンティクスは正確には異なりますが、全体的な形状は似ており、したがって多くの正しいトレードオフ/決定はすでに私たちが行ってくれていました。このドライバーを構築している間、Alyssa Rosenzweig と M1/M2 ドライバーを構築した他の人々が真の魔法使いであることが明らかになりました。彼らに敬意を表します!
成果物
Mesa: https://github.com/niklassheth/mesa
Linux カーネルドライバー: https://github.com/GravityLinux/linux/gravity-m4
ユーザー空間 RE ドキュメント(ひどい LLM のゴミの山ですが、機能します): Cody Niklas
残りの作業
Vulkan 1.4、OpenGL 4.6、OpenGL ES 3.2、OpenCL 3.1、Direct3D 12(Proton 経由)、およびレイトレーシングがすべてスコープ内です。私たちのドライバーを、世界最高のグラフィックスドライバーと同等にしたいと考えています。
さらに、Niklas と私はこれらすべてをアップストリームしたいと考えていますが、いくつかの大きな障害があります。未変更の Asahi UAPI を使用したため、Mesa のアップストリームに関するポリシー上の問題はありませんが、さらに多くのテスト、人間のレビューが必要であり、レビュー可能な PR にリファクタリングする必要があります。また、これはおそらく史上初の完全に LLM で書かれた GPU ドライバーであり、私たちのコードは人間が書いたコードよりも高い基準で評価されることを考えると、かなりの懐疑論が予想されます。これらの課題には備えていますが、それらは主に人間的で非技術的なものであり、LLM は助けられません。
Linux カーネルドライバーはさらに大きな問題になるでしょう。M1/M2 ドライバーはまだアップストリームされておらず、実際的な状況ではそれを変更できる立場にありません。ここで最善のアプローチは、そのドライバーがアップストリームされるのを待ってから、M1/M2 がアップストリームされた後(必要なリファクタリング + レビュー + 分解 + その他すべてを行った後)に私たちのドライバーをアップストリームすることだと考えています。これは残念ながら時間がかかるかもしれません。
いつ使えますか?
忍耐強い若き修行僧よ、私たちはこのコードをすぐに皆さんの手に届けられることを楽しみにしています。そして、その待ちは皆さんが予想するよりもずっと短いかもしれません。結局のところ、リンゴは木から遠くには落ちません。
参加しませんか
このプロジェクトに参加することに興味がある場合は、私たちの Discord サーバー にぜひご参加ください。アイデアを話し合ったり、チャットしたり、ただ交流したりするために、お気軽にお越しください!
追加情報: 本当かよサム?
カーネル RE タスクには、Codex と GPT-5.6 Sol(後に GPT-6 Astra がリリースされたとき)を使用しました。私が直面した最大の課題の1つは、過度に攻撃的なサイバーセキュリティ制限でした(信頼されたアクセスには登録されていません)。
単純な /goal resume または「keep going」で LLM が続行できることがほとんどでしたが(これは制限が過度に攻撃的であったことも示しています)、それでも私の自動化されたワークフローを中断しました。私は最も速く、最も愚かな可能な解決策をコーディングしました。それは、1分ごとにスクリーンショットを撮り、それを最後のスクリーンショットと比較し、同一であれば(Codex が進歩を停止したため)/goal resume と入力するデーモンです。誤って一部のグループチャットでオンにしてしまいました。
[画像: /goal resume デーモンがグループチャットにスパムを送信している様子。]
[画像: それは続いた。]
とはいえ、GPT-6 Astra と GPT-5.6 Sol は絶対に異常であり、ファームウェア ABI RE においては最高のパフォーマーであるため、このハックはそれ以上の価値がありました。
追加情報: M4 vs A18 Pro vs M5
私が知る限り、M4 と A18 Pro のユーザー空間実装は事実上同一であり、見つけられた唯一の違いは、M4 の方が値がわずかに大きいことで、コア数が多いことを示唆しています。両者のファームウェア ABI は大きく異なります。A18 Pro の2番目の RTKit コプロセッサがすべてをより複雑にしています。この点で、A18 Pro はファームウェアに関しては M4 よりも M5 に似ています。M5 のユーザー空間には M4 との類似点がいくつかあり、ISA は主にスーパーセットですが、テクスチャディスクリプタなど、一部の部分は完全に異なります。M5 のユーザー空間は部分的にリバースエンジニアリングされており、ファームウェア ABI は完全にリバースエンジニアリングされています。プロトタイプの drm-shim が構築され、徹底的にテストされており、プロトタイプを完全な Rust ドライバーに昇格させるのに時間はかからないと思います。私の主なターゲットは M4 Mac Mini および MacBook Neo のままです。
フットノート
-
Claude のヘッダーについて、これがクリーンルームではない理由がわかりません。
agx-reの「Apple バイナリを見ない」という指示について混乱したのだと思いますが、ドキュメント自体には汚染された情報は含まれていません。↩︎ -
簡単なリスト:
- ファームウェアがクラッシュした場合、唯一の回復方法は完全な再起動です。
- 作業ハンドシェイクに問題があった場合、エラーは報告されず、作業は ACK されただけで完了しませんでした。
- 一部の問題では、出力は期待どおりに変更されましたが、触れるべきではなかったページに任意のスキャンが発生しました。
-
シェーダーを含むページが見つかった場合、そのページを破棄し、追加の著作権措置として独自のシェーダーを代わりに使用しました。↩︎