CUBE SUGAR CONTAINER

技術系のこと書きます。

Mac で llama.cpp をビルドして GPU の有効・無効によるパフォーマンスの違いを比べてみる

Mac で llama.cpp を使う場合、最も手っ取り早い方法は Homebrew からインストールすることだろう。 コマンドラインから brew install llama.cpp するだけで、GPU での演算が有効なバイナリが得られる。 一方で、GPU を有効にした場合と無効にした場合で、どれくらいパフォーマンスに影響があるのかは自明でない。 そこで、今回は自分で llama.cpp をビルドして両者を比べてみることにした 1

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     26.1
BuildVersion:       25B78
$ uname -srm                                 
Darwin 25.1.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro

もくじ

下準備

まずは、llama.cpp をビルドするのに使うパッケージなどを入れる。 Homebrew が入っていないときは、あらかじめインストールしておく。

$ brew install wget curl cmake libomp

次に llama.cpp のリポジトリをクローンしておく。

$ git clone https://github.com/ggml-org/llama.cpp.git
$ cd llama.cpp

llama.cpp で動かすモデルをダウンロードする。 特に何を使っても構わないが、大きなモデルだと環境によっては多少の時間がかかる。

$ wget -P /tmp https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_0.gguf

Homebrew でインストールした OpenMP を cmake が見つけられるように環境変数を設定する。

$ export OpenMP_ROOT=$(brew --prefix)/opt/libomp

ビルドする

GPU を有効にした状態と、無効にした状態でビルドする。 Mac の場合、GPU の有効・無効は GGML_METAL という設定項目で切り替えられる。

まずは GPU を有効にした状態から。 デフォルトで GPU が有効なので、特に明示的な指定は必要ない。

$ cmake \
  -B build_metal \
  -D BUILD_SHARED_LIBS=OFF
$ cmake \
  --build build_metal \
  --config Release \
  -j \
  --clean-first \
  --target llama-cli llama-bench llama-server

次に GPU を無効にした状態を。 こちらは GPU を無効にするために、明示的に -D GGML_METAL=OFF を指定する。

$ cmake \
  -B build_no_metal \
  -D GGML_METAL=OFF \
  -D BUILD_SHARED_LIBS=OFF
$ cmake \
  --build build_no_metal \
  --config Release \
  -j \
  --clean-first \
  --target llama-cli llama-bench llama-server

ビルドすると、各ビルドディレクトリの bin ディレクトリ以下にバイナリができる。

$ ls build_metal/bin
ggml-common.h       ggml-metal-impl.h   ggml-metal.metal    llama-bench     llama-cli       llama-server

ベンチマークする

それでは llama.cpp のベンチマークツールである llama-bench を使ってパフォーマンスを確認しよう。

まずは GPU を有効にしてビルドしたバイナリから。 -m オプションでベンチマークに使うモデルの GGUF ファイルを指定する。

$ ./build_metal/bin/llama-bench \
  -m /tmp/llama-2-7b.Q4_0.gguf
ggml_metal_device_init: tensor API disabled for pre-M5 and pre-A19 devices
ggml_metal_library_init: using embedded metal library
ggml_metal_library_init: loaded in 6.543 sec
ggml_metal_rsets_init: creating a residency set collection (keep_alive = 180 s)
ggml_metal_device_init: GPU name:   Apple M2 Pro
ggml_metal_device_init: GPU family: MTLGPUFamilyApple8  (1008)
ggml_metal_device_init: GPU family: MTLGPUFamilyCommon3 (3003)
ggml_metal_device_init: GPU family: MTLGPUFamilyMetal4  (5002)
ggml_metal_device_init: simdgroup reduction   = true
ggml_metal_device_init: simdgroup matrix mul. = true
ggml_metal_device_init: has unified memory    = true
ggml_metal_device_init: has bfloat            = true
ggml_metal_device_init: has tensor            = false
ggml_metal_device_init: use residency sets    = true
ggml_metal_device_init: use shared buffers    = true
ggml_metal_device_init: recommendedMaxWorkingSetSize  = 26800.60 MB
| model                          |       size |     params | backend    | threads |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | ------: | --------------: | -------------------: |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | Metal,BLAS |       8 |           pp512 |        390.39 ± 0.23 |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | Metal,BLAS |       8 |           tg128 |         41.57 ± 0.02 |

build: db9783738 (7310)

上記に含まれるテーブルの 2 つの行が、特定の状況におけるベンチマークのスループットを示している。

まず、test カラムが pp512 となっている行は、プロンプト処理 (Prompt Processing) のスループットを示している。 プロンプト処理のスループットは、システムのピーク演算性能に影響を受けやすい。 示されている数字 (t/s) は、入力したトークン数を、プロンプトを入力してから最初のトークンが出力されるまでの時間で割ったもの。 この数字が大きいほど、TTFT (Time To First Token) が小さくなってユーザは快適に感じる。 TTFT は、モデルが長い入力を扱う場合に特に重要になる。 512 という数字は、ベンチマークで用いた入力トークン数を示している。 今回の環境では 390.39 t/s という結果が得られた。

次に test カラムが tg128 となっている行は、トークン生成 (Token Generation) のスループットを示している。 トークン生成のスループットは、システムのメモリ帯域幅に影響を受けやすい。 示されている数字 (t/s) は、出力されたトークン数を、最初のトークンが出力されてから最後のトークンが出力されるまでの時間で割ったもの。 この数字が大きいほど、TPOT (Time Per Output Token) が小さくなってユーザは快適に感じる。 TPOT は、モデルが長い出力を扱う場合に特に重要になる。 128 という数字は、ベンチマークで用いた出力トークン数を示している。 今回の環境では 41.57 t/s という結果が得られた。

ベンチマークの読み方が分かったところで、次は GPU を無効にしたバイナリでも同じことをやってみよう。

$ ./build_no_metal/bin/llama-bench \
  -m /tmp/llama-2-7b.Q4_0.gguf
| model                          |       size |     params | backend    | threads |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | ------: | --------------: | -------------------: |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | BLAS       |       8 |           pp512 |        127.29 ± 0.61 |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | BLAS       |       8 |           tg128 |         28.72 ± 0.08 |

build: db9783738 (7310)

今度は、同じテスト項目でもスループットが大きく落ちている。 プロンプト処理では 127.29 t/s と、GPU を有効にした場合に比べて約 32.6% の結果となった。 トークン生成では 28.72 t/s と、GPU を有効にした場合と比べて約 69% の結果となった。

考察

GPU を無効にしたバイナリでは、プロンプト処理とトークン生成のいずれもスループットが落ちている。 ただし、受ける影響の度合いは両者で異なっており、プロンプト処理の方がより大きくスループットを落としている。

これは、前述した通りそれぞれの処理が何処に主なボトルネックを持つかに由来するものと考えられる。 プロンプト処理の主なボトルネックは演算である。 したがって、GPU の有無に影響を受けやすい。 一方で、トークン生成の主なボトルネックはメモリの帯域である。 したがって、特にユニファイドメモリアーキテクチャの Mac では GPU の有無に影響を受けにくいのだろう。

まとめ

今回は Mac を使って llama.cpp をビルドして GPU の有効・無効によるパフォーマンスの違いを比べてみた。 GPU を無効にした場合には、プロンプト処理とトークン生成のいずれもスループットは落ちた。 一方で、スループットの落ちる程度は両者で差が見られることが分かった。



  1. どちらかというと、比べるための方法をメモしておく意味合いが強い