Technically Impossible

Lets look at the weak link in your statement. Anything "Technically Impossible" basically means we haven't figured out how yet.

GPU無し、コンテナもPythonも使わない、RAM=8GBのLinux PCでGPT

2023年03月23日追記

この投稿での事例は、1トークンの推論に約1分を要する。とにかく動作させてみることを念頭にしている。swapを用いず、小規模なモデルをオン・メモリで動作させることで、実用に適うパフォーマンスで動作させることができる。そのような事例を、次の投稿で紹介している。関心があれば参照してほしい。
impsbl.hatenablog.jp

本文

いわゆる「AI」をPCで運用するには、GPUとVRAMをはじめとする潤沢な計算リソースが求められる。"ggerganov/ggml"*1を利用すると、GPT (Generative Pre-trained Transformer)のように大規模言語モデルに基づいた推論を、普及機レベルのPCでも動かすことができる。

とはいえ最初に触れておくと、この投稿で紹介している方法では、動作はしているものの、実用的なものではない。先日、Raspberry Piで動作させたというTwitterを見かけた*2。このパフォーマンスが、1トークン当たり10秒であるのに対して、この投稿の環境では1トークン当たり1分を要している。動きはするが、はるかに遅いのだ。ちなみにggerganov氏のMacBookでは125ミリ秒だ。

On my 32GB MacBook M1 Pro, I achieve an inference speed of about 125 ms/token or about ~6 words per second

ggml/examples/gpt-j at master · ggerganov/ggml · GitHub

"ggerganov/ggml"に含まれる"gpt-j"は、16GB RAMを要求される。実用性は置いて、動作させるだけならば8GB RAMを搭載したPCでも、8GB swapを活用することで動作させることができる。とりあえず動かし、そのパフォーマンスを確認するのが、今回の投稿だ。

次のスペックの環境で検証している。

OS Clear Linux 38590*3
CPU Intel Core i5-8250U
RAM 8GB
Storage SSD: 256GB

ggmlのビルド、インストール

cmakeとmakeを搭載しているLinux環境であれば、簡単にggmlをビルド、インストールできる。GitHubページに掲載されているように、

  1. ソースコードをダウンロードする
  2. ディレクトリを作る
  3. cmake、makeの実行

を順番にコマンド実行すればよい。"ggml"をホーム・ディレクトリにダウンロードし、"gpt-j"だけをビルドするには、次のコマンドを実行する。"gpt-j"がディレクトリ"~/ggml/build/bin/へ出力される。

cd ~
git clone https://github.com/ggerganov/ggml
cd ggml
mkdir build
cd build
cmake ..
make -j4 gpt-j

GPT-J 6Bモデルのダウンロード

"gpt-j"を実行するにはGPTモデルが必要だ。そのダウンロードに用いる"download-ggml-model.sh"が用意されているのだが、この投稿では使用しない。このモデルのサイズは12GBあり、馴染みの方法で直接ダウンロードしたほうが良いと思うのだ。Hugging Faceから"ggml-model-gpt-jt-6B.bin"を直接ダウンロードすることにした。ダウンロードしたモデルは、"~/ggml/build/bin/"へ保存する。

huggingface.co

swapの作成

GitHubに記載されているように、GPTモデルをメモリ上へロードするために、"gpt-j"は16GB RAMを要求する。

No video card required. You just need to have 16 GB of RAM.

ggml/examples/gpt-j at master · ggerganov/ggml · GitHub

必要なメモリ領域を確保できなければ、segmentation faultによりプログラムが終了する。このような事態を避けるために、swap領域を作成する。次のコマンドで、スワップ・ファイル"swap.img"を"~/ggml/build/bin/"に作成している。

sudo dd if=/dev/zero of=~/ggml/build/bin/swap.img bs=1M count=8096

sudo chown 0:0 ~/ggml/build/bin/swap.img
sudo chmod 600 ~/ggml/build/bin/swap.img

sudo mkswap ~/ggml/build/bin/swap.img
sudo swapon ~/ggml/build/bin/swap.img

"gpt-j"の実行

"gpt-j"を実行する前に、そのオプションの一部について触れておく。これらは"examples/utils.cpp"に定義されている。

-m GPTモデルのパス
-n トークン数token
言うなれば、返答の単語数
-p プロンプト
-t 処理スレッド数

この投稿では"gpt-j"とモデルは同じパスに保存されている。従って、典型的なコマンドは

cd ~/ggml/build/bin
./gpt-j -m ggml-model-gpt-j-6B.bin -p "hello"

処理パフォーマンスを比較するため、異なるオプションで実行する。

./gpt-j -n 5 -t 4 -m ggml-model-gpt-j-6B.bin -p "hello"
./gpt-j -n 5 -t 8 -m ggml-model-gpt-j-6B.bin -p "hello"
./gpt-j -n 10 -t 4 -m ggml-model-gpt-j-6B.bin -p "hello"

処理スレッド数を増やすと、トークン当たりの予測時間を短縮できる。一方、トークン数が予測時間に影響を与えることはない。検証機は、トークン一つの予測に約1分を要しているのが分かる。

デフォルト設定でコマンド実行した場合、その終了までに約4時間を要した。

./gpt-j -m ggml-model-gpt-j-6B.bin -p "hello"

🔎result of the commands above

Google Colabで実行する

"gpt-j"はGoogle Colab*4でも実行できるのだが、そのプロセスは開始数分で強制終了される。

!git clone https://github.com/ggerganov/ggml
%cd /content/ggml
%mkdir build
%cd build
!cmake ..
!make -j4 gpt-j

!../examples/gpt-j/download-ggml-model.sh 6B
!./bin/gpt-j -m models/gpt-j-6B/ggml-model.bin -p "who are you?"
!./bin/gpt-j -m models/gpt-j-6B/ggml-model.bin -p "Do you know what day today is?"

余談 - Apple M1 CPU、GPUの併用と限界

ggerganov氏がggmlを記述した動機は、自身のMacBookでモデル推論を動作させることだった。特に「Implementation details」で語られていることが興味深い。ARM NEONによる128bit演算がM1 CPU上で動作する。同時に、M1 GPUに対して一部の処理を任せようとしてもパフォーマンスが向上しないのだという。

その理由として、M1 CPUとM1 GPUが共有するメモリ空間とチャネルにて、特にCPU側とのやり取りでメモリ帯域が占有されていたからではないか、と推測されている。さらに、CPUとGPUにパフォーマンス上の違いがないこともあり、併用しても意味がないことにも触れられている。

However, to my surprise, using MPS together with the CPU did not lead to any performance improvement at all. My conclusion was that the 8-thread NEON CPU computation is already saturating the memory bandwidth of the M1 and since the CPU and the GPU on the MacBook are sharing that bandwidth, it does not help to offload the computation to the GPU. Another observation was that the MPS GPU matrix multiplication using 16-bit floats had the same performance as the 8-thread NEON CPU implementation. Again, I explain this with a saturated memory channel. But of course, my explanation could be totally wrong and somehow the implementation wasn't utilizing the resources correctly.

In the end, I decided to not use MPS or the GPU all together.

ggml/examples/gpt-j at master · ggerganov/ggml · GitHub

*1:github.com

*2:

twitter.com

*3:clearlinux.org

*4:colab.research.google.com