🍺

PodmanのコンテナからmacOSのApple Silicon GPUを使ってAIワークロードを高速処理できるようになりました

2024/09/13に公開

はじめに

macOS上のPodmanで、コンテナからApple Silicon MacのGPUにアクセスすることができるようになりました。macOS上でPodmanを実行する際は、実際はPodman Machineと呼ばれるLinux仮想マシンが起動し、その中で動くPodmanに対してREST APIで接続することで、Linuxコンテナを実行します。「コンテナからMacのGPUにアクセスする」ということは、Podman Machine (つまりmacOS上の仮想マシン) からGPUにアクセスすることを意味します。

macOS上でコンテナを動かすためのツールとしては、DockerやLima + containerd + nerdctlが挙げられますが、おそらく2024年8月現在、コンテナからApple Silicon GPUを使えるのはPodmanだけです(私が調べた限り...うそだったらごめんなさい)。

本記事は、2部構成になっています。

  • 前半: Podman + libkrunでコンテナからGPUを利用する方法
  • 後半: Podman + libkrunによってコンテナからGPUを利用できるようになる仕組み、もしくはなぜDockerやLimaではコンテナ(Linux仮想マシン)からGPUが使えないか

Podman + libkrunでコンテナからGPUを利用する方法

2部構成の前半、とりあえず実際に動かしてみます。

準備

セットアップ方法は大きく3種類あります。

  1. brewを使ってpodman, krunkitをインストールする
  2. pkg形式のインストーラを使用する
  3. Podman Desktopからセットアップする

brewを使ったインストール

"Podman and Libkrun" というブロク記事にしたがって、podmanとkrunkitをbrew installします。

https://blog.podman.io/2024/07/podman-and-libkrun/

% brew install podman
% brew tap slp/krunkit
% brew install krunkit

さらに、~/.config/containers/containers.conf に以下を記述します (このファイルがない場合は作成してください)。

[machine]
provider="libkrun"

最後にPodman Machineを作成&起動します。

% podman machine init --now

(podman machine init --now は、podman machine init して podman machine start するのを一度にやるコマンドです)

pkg形式のインストーラを使ったインストール

v5.2以降であれば、公式のリリースページにあるpkg形式のインストーラ(本記事執筆時点の最新版はv5.2.2のpodman-installer-macos-arm64.pkg)をダウンロードして実行します。
Podmanだけでなくkrunkitも自動的にインストールされます。

インストールが終わったら、brewで入れたときと同じように ~/.config/containers/containers.conf を作成し、podman machine init --now を実行します。

Podman Desktopを使ったセットアップ

PodmanのGUIアプリであるPodman Desktopも、 v1.12からlibkrunを使ったPodman Machineに対応しています。

https://podman-desktop.io/blog/podman-desktop-release-1.12

(どうでもいいですが、このグラボを持ったセルキーかわいいですね)

Podman Desktopのインストール方法は公式ドキュメントをご参照ください (dmgをダウンロードする、もしくはbrewを使います)。

Podman Desktop v1.12をご使用の場合は、Podman Machineを作成するときに Provider Type として GPU enabled (LibKrun) を選択すると、libkrunを使ったPodman Machineが起動します。

確認

上記のいずれかの方法でセットアップすると、libkrun providerを使ったPodman Machineが起動します。このPodman Machineに入ると、/dev/dri 以下にGPU関連のデバイスファイルが生えていることがわかります。

% podman machine ssh
Connecting to vm podman-machine-default. To close connection, use `~.` or `exit`
Fedora CoreOS 40.20240808.2.0
Tracker: https://github.com/coreos/fedora-coreos-tracker
Discuss: https://discussion.fedoraproject.org/tag/coreos

core@localhost:~$ find /dev/dri -type c -or -type l
/dev/dri/renderD128
/dev/dri/card0
/dev/dri/by-path/platform-a007000.virtio_mmio-render
/dev/dri/by-path/platform-a007000.virtio_mmio-card
core@localhost:~$ exit
logout

念のため、(GPUを使わない普通の)コンテナを実行できることを確認しておきます。

% podman run --rm hello-world
Resolved "hello-world" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull quay.io/podman/hello:latest...
Getting image source signatures
Copying blob sha256:1ff9adeff4443b503b304e7aa4c37bb90762947125f4a522b370162a7492ff47
Copying config sha256:83fc7ce1224f5ed3885f6aaec0bb001c0bbb2a308e3250d7408804a720c72a32
Writing manifest to image destination
!... Hello Podman World ...!

         .--"--.
       / -     - \
      / (O)   (O) \
   ~~~| -=(,Y,)=- |
    .---. /`  \   |~~
 ~/  o  o \~~~~.----. ~~
  | =(X)= |~  / (O (O) \
   ~~~~~~~  ~| =(Y_)=-  |
  ~~~~    ~~~|   U      |~~

Project:   https://github.com/containers/podman
Website:   https://podman.io
Desktop:   https://podman-desktop.io
Documents: https://docs.podman.io
YouTube:   https://youtube.com/@Podman
X/Twitter: @Podman_io
Mastodon:  @Podman_io@fosstodon.org

AI/MLワークロードの実行

llama.cppを使ったAI/MLワークロードをコンテナで実行してみます (現時点の実装では、GPGPUとしては利用できますが、3Dグラフィックスのアクセラレーションはできません)。

事前にモデルを ~/Downloads にダウンロードしておきます (下の例では Llama-3-ELYZA-JP-8B-q4_k_m.gguf を使っています)。
/dev/dri をホストデバイスとしてコンテナに追加し(ここで言う「ホスト」はPodman Machineのことです)、さらに ~/Downloads をコンテナ内にbind mountしてPodmanを実行します。

% podman run --rm -ti --device /dev/dri -v ~/Downloads:/models:Z quay.io/slopezpa/fedora-vgpu-llama bash

コンテナ内で vulkaninfo を実行すると、Virtio-GPU Venus (Apple M2) として準仮想化GPUデバイスが見えていることがわかります。

[root@5528136b7365 /]# vulkaninfo --summary

<略>

Devices:
========
GPU0:
	apiVersion         = 1.2.0
	driverVersion      = 23.3.5
	vendorID           = 0x106b
	deviceID           = 0xe0603f0
	deviceType         = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
	deviceName         = Virtio-GPU Venus (Apple M2)
	driverID           = DRIVER_ID_MESA_VENUS
	driverName         = venus
	driverInfo         = Mesa 23.3.5
	conformanceVersion = 1.3.0.0
	deviceUUID         = 901f9c56-e0b4-ba61-62fb-f05d68e99066
	driverUUID         = d2076857-3352-aaf9-a5ba-778bcb213a61

<略>

llama.cppで推論をしてみます。

[root@5528136b7365 /]# main --temp 0 -m models/Llama-3-ELYZA-JP-8B-q4_k_m.gguf -b 512 -ngl 99 -p "Podmanのlibkrun providerについて教えて下さい"
Log start
main: build = 2238 (56d03d92)
main: built with cc (GCC) 13.2.1 20231205 (Red Hat 13.2.1-6) for aarch64-redhat-linux
main: seed  = 1725466249
ggml_vulkan: Found 1 Vulkan devices:
Vulkan0: Virtio-GPU Venus (Apple M2) | uma: 1 | fp16: 1 | warp size: 32
llama_model_loader: loaded meta data with 22 key-value pairs and 291 tensors from models/Llama-3-ELYZA-JP-8B-q4_k_m.gguf (version GGUF V3 (latest))
llama_model_loader: Dumping metadata keys/values. Note: KV overrides do not apply in this output.
llama_model_loader: - kv   0:                       general.architecture str              = llama
llama_model_loader: - kv   1:                               general.name str              = Llama-3-8B-optimal-merged-stage2

<略>

system_info: n_threads = 4 / 4 | AVX = 0 | AVX_VNNI = 0 | AVX2 = 0 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 0 | NEON = 1 | ARM_FMA = 1 | F16C = 0 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | MATMUL_INT8 = 0 |
sampling:
	repeat_last_n = 64, repeat_penalty = 1.100, frequency_penalty = 0.000, presence_penalty = 0.000
	top_k = 40, tfs_z = 1.000, top_p = 0.950, min_p = 0.050, typical_p = 1.000, temp = 0.000
	mirostat = 0, mirostat_lr = 0.100, mirostat_ent = 5.000
sampling order:
CFG -> Penalties -> top_k -> tfs_z -> typical_p -> top_p -> min_p -> temperature
generate: n_ctx = 512, n_batch = 512, n_predict = -1, n_keep = 0


Podmanのlibkrun providerについて教えて下さい。

libkrunはKDEのRun Commandを提供するライブラリです。libkrun providerは、libkrunが提供するRun Commandを実行するためのプロバイダーです。

libkrun providerは、libkrunが提供するRun Commandを実行するために必要な情報を提供します。例えば、実行するコマンドのパスや環境変数などです。

<略>

llama_print_timings:        load time =   16897.88 ms
llama_print_timings:      sample time =     475.69 ms /   307 runs   (    1.55 ms per token,   645.38 tokens per second)
llama_print_timings: prompt eval time =    4442.21 ms /    12 tokens (  370.18 ms per token,     2.70 tokens per second)
llama_print_timings:        eval time =   26072.46 ms /   306 runs   (   85.20 ms per token,    11.74 tokens per second)
llama_print_timings:       total time =   31345.05 ms /   318 tokens
Log end

無事実行することができました (回答はデタラメですが)。macOS上でアクティビティモニタを見ると、実際にGPUを使っていることがわかります。


コンテナからGPUを使う仕組み

2部構成の後半です。ここからは、libkrunを使うとコンテナからGPUを使えるようになる技術的な背景などを書きます。

なぜlibkrunか

冒頭で書いたとおり、PodmanをmacOSで使うときは、Linux仮想マシンを起動し、その仮想マシンに対してREST API接続することでコンテナを実行します。Podmanはv0.8.2からmacOSに対応していますが、Podman Machineの実装としては、

  • v0.8.2〜v4.9: Qemu + Hypervisor.framework (-accel hvf つきで起動します[1])
  • v5.0以降: Virtualization.framework

をVMMとして使います。

Podman MachineからGPUを使う方法を検討したところ、いずれの方法でも短期的には解決できそうにない課題があったため、第3の選択肢として、libkrunをVMMとして使うことになりました。

まず最初にQemu、Virtualization.frameworkそれぞれの課題を整理...する前に、前提知識としてmacOSの仮想化機構について簡単にまとめます。

macOSの仮想化機構

macOSは、仮想化に関してHypervisor.frameworkVirtualization.frameworkという2つのフレームを持っています。Hypervisor.frameworkは主にCPUやメモリの仮想化などの低レイヤ/カーネル側を担当するフレームワークで、LinuxにおけるKVMに相当します。Virtualization.frameworkは主に仮想デバイス等を提供する高レイヤ/ユーザーランド側のフレームワークで、LinuxにおけるQemuに相当します。便宜上、本記事では前者を「ハイパーバイザー」、後者を仮想マシンモニター(VMM)と呼びます。

macOS Linux
ハイパーバイザー(CPU/メモリの仮想化等) Hypervisor.framework KVM
VMM(仮想デバイスの管理等) Virtualization.framework Qemu


macOSのHypervisor.frameworkとLinux KVM[2]

UTM, LimaといったmacOS上でよく使われる仮想化管理ツールの多くは、Virtualization.frameworkを使っています (設定でQemuと選べますがだいたいVirtualization.frameworkを使いますよね...きっと)。対して、xhyveやQemuはHypervisor.frameworkを使います。

Hypervisor.framework Virtualization.framework
仮想化管理ツール xhyve, Qemu, libkrun, ... vfkit, UTM, Lima, ...


macOS上のQemuとHypervisor.framework[3]

macOS上のVirtualization.framework[4]

Linuxゲストに対するGPUサポート

ざっと調べた限り、様々な仮想化ツールで、macOS上のLinuxゲストからGPUを使うことについて議論されています。

Podman MachineからGPUを使うにあたり、VMMとしてQemuを使った場合とVirtualization.frameworkを使う場合のそれぞれについて、どういう課題があるかをまとめました。

Qemuの課題

Apple Siliconが出た当初から使われている代表的なVMMとして、Qemuがあります。しかし、Qemuにはいくつかの課題があります。

  • TSO (Total Store Ordering) / Rosettaサポートがない
  • virtiofsがないので、virtio-9pを使うしかない (virtio-9pは遅いしSELinuxサポートに課題)

macOS用Podmanは、上で書いたように最初はQemuを使っていましたが、v5.0からVirtualization.frameworkを使った仮想化に切り替えました。理由は、Qemuの不具合でしばしばmacOS用Podmanが動かなくなり、その対応に苦慮していたためです。

QemuのLinuxゲストからmacのGPUを使う実装はあるようです。

https://gist.github.com/akihikodaki/87df4149e7ca87f18dc56807ec5a1bc5

https://github.com/knazarov/homebrew-qemu-virgl

しかし、Podman MachineでGPUを使う方法を検討していたのは2024年初頭で、当時既にVirtualization.frameworkベースのPodman Machineに移行することになっていたこともあり(2024年3月末にVirtualization.frameworkに移行したv5.0が出ました)、PodmanのGPU対応をQemuで頑張るのは避けたかったようです。

Virtualization.frameworkの課題

v5.0以降でPodman MachineはVirtualization.frameworkベースに移行し、Qemuに起因する課題の多くは解決しました。

  • VMMとして安定している
  • virtiofsが使える
  • Rosettaが使える

一方で、いくつか新たな課題が見えてきました。

  • GPUを使えない
  • Virtualization.frameworkが提供するvirtiofsの課題がある

実は、Virtualization.frameworkは準仮想化GPUデバイスを提供していますが(ParavirtualizedGraphics.framework)、Linux用のデバイスドライバがありません。また、仮にLinux用のデバイスドライバがあったとしても、Linuxで動くMetal.frameworkがないこと、仮想GPU ↔ ホストGPU間のコマンド転送のシリアライズプロトコルが不明であること、などから、Virtualization.framework上のLinux仮想マシンでGPUを使えるようにするためには、かなりハードルが高そうです (実際、Virtualization.frameworkを使う仮想化ツールの中で、LinuxゲストでGPUが使えるものはまだないはずです)。

また、Virtualization.frameworkのvirtiofs実装にはいくつか課題があることがわかってきました。具体的には同時書き込みに関する制限や、SELinuxと組み合わせたときの動き等です。Virtualization.frameworkはAppleのプロプライエタリなソフトウェアであるため、修正するにはAppleに要望を出して結果を待つことしかできないのがつらいところです。

libkrunという選択肢

libkrunというライブラリ形式のVMMがあります[5]。Linux KVMだけでなくmacOSのHypervisor.frameworkのサポートが入っていること、virtio-gpuが使えること、OSSであること(メインの開発者はRed Hatの人です)、ということから、libkrunを使った仮想マシンでMacのホストGPUを使えるか検討することになりました。

余談 (libkrunでvirtio-gpuが使えるようになったきっかけは「Asahi Linux上でSteamゲームを実行したかったから」という小噺)

macOS用PodmanでのGPU利用方法を検討していた時点(2024年初め頃)で、libkrunには既にLinuxホスト上のマイクロVMでのvirtio-gpuが使えるようになっていたのですが、その理由は「Asahi Linux (Apple Silicon Mac用のLinuxディストリビューション) 上でSteamゲームを実行したかったから」です。

libkrun開発者のブログ記事に詳細が載っていますが、Asahi LinuxでSteamゲームを実行するために、libkrunに対して以下の機能追加を行いました。

  • Asahi Linuxはページサイズが16KBだが、x86のゲームバイナリを動かすにはページサイズを4KBにする必要がある。一方で、 Apple Silicon搭載MacはIOMMUチップが16KBページで動くことが前提となっており[6]、実質4KBページサイズのカーネルは(少なくとも現状では)動かない[7]。この対応策として、Asahi Linux上でlibkrunを使って4KBページサイズのaarch64 LinuxのmicroVMを起動する
  • x86コードの実行にはFEX-Emuを使う (FEX-Emuはaarch64上でx86コードを実行するためのエミュレータ、いろいろ最適化が施されており、Qemuよりも高速に実行できる、TSOサポートもあり)
  • virtio-gpuの実装はcrosvmrutabaga_gfx crateを流用できそう (PR)
    • そのままだとゲームをするにはパフォーマンスが足りないので、AGX (Asahi LinuxのGPUドライバ)、Mesa、virglrendererを改良してDRM Native Context[8]が使えるようにする
  • Steamの実装上の事情で、TSOではダメでvirtio-netが必要になった(が既に実装済みだった) (PR)
    • 元々libkrunは、vsockとTSI (Transparent Socket Impersonation) という独自技術を組み合わせて、virtio-netを使わずにネットワーク通信を実現していました[9]
  • virtio-sndも実装しちゃえ (PR)

そんなこんなで、今ではAsahi Linux上で多くのSteamのゲームを楽しめるようになっています。

https://x.com/slpnix/status/1704413076320117096

https://x.com/LinaAsahi/status/1794408897572462669

https://x.com/LinaAsahi/status/1795777842653065623

このときに実装したvirtio-gpuを、mac用PodmanでGPUを利用するにあたって活用することができました。

(余談おわり)

Podman Machineをlibkrunで動かすために

v5.0以降のPodmanは、vfkitという、Virtualization.frameworkのGo bindingであるvzを使ったCLIを使って仮想マシンを起動します。

libkrunをPodman Machineのproviderにするにあたって、krunkitというlibkrunのwrapperとなるCLIを作りました。Podman側の変更を最小限に抑えるために、krunkitにはvfkitとほぼ同じコマンドラインオプションが実装されています。

元々libkrunは、BIOS/UEFI含めた完全な仮想ハードウェアを作るものではなく、必要最低限の機能を提供する軽量なVMMでした。また当初はLinux KVMを前提としていましたが、2021年にHypervisor.frameworkのサポートが加わり、macOS上でもマイクロVMを動かせるようになっていました。

macOS上でのLinuxゲストからのGPUアクセスをサポートするにあたり、

を追加することにより、最終的にLinuxゲストからGPUを使えるようになりました。

Linuxゲスト上のアプリケーションがVulkan APIでvirtio-gpuにアクセスすると、VulkanコマンドがVenusプロトコルでシリアライズされてVMMのvirglrendererにわたり、さらにMoltenVKを使ってMetal APIに変換してGPUハードウェアにアクセスします。


Linuxゲストから仮想化GPUを使用するときのソフトウェアスタック

llama.cpp入りコンテナイメージ

本記事前半での動作確認は、quay.io/slopezpa/fedora-vgpu-llama をコンテナとして使用しましたが、同じようなことをするコンテナイメージを作ってみました。

https://github.com/orimanabu/fedora-llama-vulkan

中でやっているのは、ちょっと古いllama.cppをVulkanを使うようにコンパイルしているだけです。MesaのVulkanドライバは、メモリ確保のサイズを16KB単位になるようアラインメント調整する等、いくつかのパッチを当てたものを使っています[10]

参考文献

脚注
  1. https://github.com/containers/podman/blob/v4.9.5/pkg/machine/qemu/options_darwin_arm64.go#L18 ↩︎

  2. 図はKVM Forum 2022のセッション「Hypervisor.Framework - Virtualization on macOS」より引用 (リンク先は動画です、スライドは公開されていないようでした) ↩︎

  3. 図はKVM Forum 2023のセッション「macOS in QEMU (ARM edition)」のp.3より引用 ↩︎

  4. 図はKVM Forum 2023のセッション「macOS in QEMU (ARM edition)」のp.4より引用 ↩︎

  5. libkrunの詳細については https://logmi.jp/tech/articles/324735 https://rheb.hatenablog.com/entry/libkrun-intro をご参照ください。 ↩︎

  6. 詳細は https://asahilinux.org/2021/10/progress-report-september-2021/#linux-drivers-galore の "IOMMU 4K patches" をご参照ください ↩︎

  7. 対応パッチが提案されていますが、まだ取り込まれていないようです: [PATCH v3 0/6] Support IOMMU page sizes larger than the CPU page size ↩︎

  8. DRM Native ContextはGoogleが開発したvirtio-gpu高速化技術。https://indico.freedesktop.org/event/2/contributions/53/attachments/76/121/XDC2022_ virtgpu drm native context.pdf 既存のAPI Remotingの手法 (VMからのOpenGLやVulkanのAPI呼び出しを全てホストに転送して描画する) ではオーバーヘッドが大きいが、DRM Native Contextでは実際にGPU操作を行うコマンドのみをホストに送ることで処理を効率化し、ほぼ物理アクセスに近い性能が出せるようになる。現状、QualcommのfreedrenoとAMDのamdgpuがDRM Native Contextに対応している ↩︎

  9. TSIを使ったlibkrunのネットワーク通信については https://rheb.hatenablog.com/entry/libkrun-networking をご参照ください ↩︎

  10. パッチの当たったMesaのrpmはslp/mesa-krunkitのcoprにあります。パッチの内容は https://copr-dist-git.fedorainfracloud.org/cgit/slp/mesa-krunkit/mesa.git/tree/?h=f40 をご参照ください。 ↩︎

GitHubで編集を提案

Discussion