!!注意!!: この記事で取ったベンチマークは、片方のGPUのみECC有効となっていたことが判明しました。PCIeのレーン数も双方のGPUで異なっており、非常にアンバランスな条件で測定した結果となっておりますので、マルチGPUの効果を見るのにはあまり適しておりません。後記事にてEPYCを購入し、ちゃんとECCを無効化した上で両GPUともPCIe x16で接続した結果を公開しましたので、こちらも合わせて見ていただければと思います。
前記事:
後記事 :
前書き
きっかけは、llama2の13BモデルがシングルGPUでは動かなかったこと。
以前記事にした鯖落ちP40を利用して作った機械学習用マシンですが、最近分析界隈でも当たり前のように使われ始めているLLMを動かすことを考えると、GPUはなんぼあってもいい状況です。
また、私自身マルチGPUを扱った経験が薄く、精々SageMaker上でPyTorchのDPでお手軽マルチGPUトレーニングをやったことがある程度。今後大きなモデルを扱っていくことを考えると、お家でマルチGPUを試せる環境を準備しとくのは悪くないと思ってしまった。
そこで、P40をもう一台買い増してみることにしたので、色々とベンチやマルチGPUの使い勝手を書いておこうと思います。
まとめ
- 私の使っているB550マザーでは1本目のPCIeがx16, 2本目がx4で分割もできないようなので、仕方無しに2枚目のP40はx4接続となりました。
- フルスピードで動かすならThreadripperかEPYCがほしいところ。昔持ってたやつ売らなければ良かった......。
- テスト項目は次の3つとしました。
- 1.Transformers公式のDDPチュートリアル(GPT2のトレーニング): 一番実用に近いベンチ。
結果的にシングルGPUでもトレーニングできてしまったのですが、マルチのほうが約25%遅い結果に。結果をちゃんと見てみたところ、train_samples_per_second
の値は約1.6倍となっていることが判明。Gradientsを共有するのに時間がかかっているのであろうか? - 2.Stable Diffusion SDXL base 1.0 画像生成速度(シングルGPU性能): 512x512の画像生成が大体11.6 secでした。StableDiffusion-webuiがマルチGPUに対応しているわけではないですが、別プロセスで並列して生成させることはできるはず。
- 3.llama.cppでシングルVSマルチGPU性能比較: llama.cppをcuBLAS有効でコンパイルし、
llama-2-13b-chat.ggmlv3.q8_0.bin
をGPUなし/1GPU/2GPUで動作させて実行時間を測りました。結果、5%ほど速いスピードで推論できることがわかりました。
- 1.Transformers公式のDDPチュートリアル(GPT2のトレーニング): 一番実用に近いベンチ。
PCIeの帯域が問題なのか、あまりうまくスケールしていない感じです。これは中古EPYC買うフラグが立ってしまったかもしれません。
とはいえとりあえずマルチGPUの意味は確認できたので、あとは実用的にKaggleコンペなどで使ってメリットを出していきたいと思います。
直近では次のコンペに出てみようと思ってますので、ここでP40x2を有効活用することができれば嬉しいなあ。
セットアップ
すでに前の記事で構築済みのマシンに、お手製ブラケットでファンを取り付けたP40を増設しました。特にOS上は設定変更などいらずに2枚目のGPUが認識されるようになりました。
1. Transformers公式のDDPチュートリアル
準備
こちらのチュートリアルに、TransformersでマルチGPUを使ってトレーニングを行う方法が紹介されています。ここのコードをベンチマークに利用しました。
ここのチュートリアルのコードを実行するためには、ソースからTransformersをインストールしなければならないなど少し工夫が必要です。新たなPython仮想環境上で次の手順にて準備を行います(最後のPyTorchインストールは各々のCUDAバージョン等に合わせて変更してください)。
pip install git+https://github.com/huggingface/transformers pip install evaluate datasets pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
ベンチマークに用いるrun_clm.py
は、transformersの公式レポジトリからクローンしてきます。
git clone https://github.com/huggingface/transformers.git
で、あとは次のコードを実行するのですが、
NCCL_P2P_DISABLE=1 CUDA_VISIBLE_DEVICES=0,1 \ python -m torch.distributed.launch --nproc_per_node 2 examples/pytorch/language-modeling/run_clm.py \ --model_name_or_path gpt2 --dataset_name wikitext --dataset_config_name wikitext-2-raw-v1 \ --do_train --output_dir ~/tmp/test-clm --per_device_train_batch_size 4 --max_steps 200
torch 2.0ではtorch.distributed.launchからtorchrunへの移行が推奨されているようなので、エラーが発生してしまいました。
ValueError: Some specified arguments are not used by the HfArgumentParser: ['--local-rank=0']
これを回避するため、torch.distributed.launch
からtorchrun
へコマンドを単純に差し替えたところ、問題なく動いてくれました。
NCCL_P2P_DISABLE=1 CUDA_VISIBLE_DEVICES=0,1 \ torchrun --nproc_per_node 2 examples/pytorch/language-modeling/run_clm.py \ --model_name_or_path gpt2 --dataset_name wikitext --dataset_config_name wikitext-2-raw-v1 \ --do_train --output_dir ~/tmp/test-clm --per_device_train_batch_size 4 --max_steps 200
結果
マルチGPUの結果
***** train metrics ***** epoch = 0.69 train_loss = 3.279 train_runtime = 0:03:37.12 train_samples = 2318 train_samples_per_second = 7.369 train_steps_per_second = 0.921 [INFO|modelcard.py:452] 2023-08-05 16:27:29,020 >> Dropping the following result as it does not have all the necessary fields: {'task': {'name': 'Causal Language Modeling', 'type': 'text-generation'}, 'dataset': {'name': 'wikitext wikitext-2-raw-v1', 'type': 'wikitext', 'config': 'wikitext-2-raw-v1', 'split': 'train', 'args': 'wikitext-2-raw-v1'}}
結果は217sとのこと。Titan RTX V x2のベンチ結果がTransformers公式に載っているのですが、それだと131s
らしいのでまあ早くも遅くもない?
シングルGPUで同じトレーニングを実施。むしろ若干早く(173s)でトレーニングできてしまっているので、2GPUの恩恵を感じるほどの結果ではなくなってしまった……。やはり片方のPCIeの帯域がx4なのが悪いのか。
ただ、train_samples_per_second
の値は約1.6倍となっているので、1秒間に処理できているサンプル数は増加している模様。NVLinkがないことから、GPU間でのパラメータ共有に時間がかかっていたりするのだろうか?まあ、train_samples_per_second
の算出方法がわからないのでなんとも言えないが……。
続いて、同じベンチマークコードをシングルGPUで実行します。単にCUDA_VISIBLE_DEVICES=0
を頭につけて1枚だけGPUを認識できるようにし、nproc_per_node
を1にしただけです。
NCCL_P2P_DISABLE=1 CUDA_VISIBLE_DEVICES=0 torchrun --nproc_per_node 1 examples/pytorch/language-modeling/run_clm.py \ --model_name_or_path gpt2 --dataset_name wikitext --dataset_config_name wikitext-2-raw-v1\ --do_train --output_dir ~/tmp/test-clm --per_device_train_batch_size 4 --max_steps 200 ***** train metrics ***** epoch = 0.34 train_loss = 3.3276 train_runtime = 0:02:53.38 train_samples = 2318 train_samples_per_second = 4.614 train_steps_per_second = 1.154
ちなみに、ちゃんと2GPU使用時には、どちらのGPUの利用率も高くなっていることが確認できました。
2. Stable Diffusion SDXL base 1.0でのP40の性能計測
準備
インストールは次のページを参照。唯一、異なるPCのWebUI環境にアクセスする必要があるので、launch.py
の引数に--listen
を追加しました。
sudo apt install curl gnupg2 git python-is-python3 python3.10-venv python3-pip git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui cd stable-diffusion-webui python launch.py --listen
また、モデルファイルとしては次のURLから対象のsafetensors
をダウンロードして、models/Stable-diffusion
に配置しました。
https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/tree/main https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/tree/main
sd_xl_base_1.0.safetensors sd_xl_refiner_1.0.safetensors
結果
txt2imgで512x512のイメージを生成した際の生成時間を確認したところ、 11.6 secとのこと。
当然2枚GPUが刺さっていれば別プロセスとして違うGPUを使うように設定してあげて倍の枚数生成できると思うので、悪くないのではないだろうか。
3. llama.cpp でマルチGPUとシングルGPUを比較する
準備
llama.cppでGPUを使う方法に関しては次のページを参照しました。
次の手順でcuBLASを有効化したllama.cppをコンパイルします。
git clone https://github.com/ggerganov/llama.cpp cd llama.cpp mkdir build cd build cmake .. -DLLAMA_CUBLAS=ON cmake --build . --config Release
モデルとしてはllama2のGGML 8bit量子化モデル(llama-2-13b-chat.ggmlv3.q8_0.bin
)を次のページからダウンロード
https://huggingface.co/TheBloke/Llama-2-13B-chat-GGML/tree/main
結果
とりあえず200文字程度の詩を生成させてみて、その際のトークン生成速度(eval time)を比較しました。
CPU(5900X) | 1GPU | 2GPU |
---|---|---|
2.76 | 14.83 | 15.59 |
結果、5%ほど2GPUのほうが速くなりました。llama.cppの場合多分層ごとに異なるGPUに配置するような形になるため、あんまり効率は良くないのかもしれません。シンプルに1GPUに乗り切らないモデルを動かしたりできるという面で2GPUの価値はあるでしょう。
multi GPU
./build/bin/main -m ./models/llama-2-13b-chat.ggmlv3.q8_0.bin --temp 0.1 -p "### Instruction: Create poetry about Mount Fuji in 200 words ### Response:" -ngl 40 -b 512 main: build = 952 (3323112) main: seed = 1691226913 ggml_init_cublas: found 2 CUDA devices: Device 0: Tesla P40, compute capability 6.1 Device 1: Tesla P40, compute capability 6.1 llama.cpp: loading model from ./models/llama-2-13b-chat.ggmlv3.q8_0.bin llama_model_load_internal: format = ggjt v3 (latest) llama_model_load_internal: n_vocab = 32000 llama_model_load_internal: n_ctx = 512 llama_model_load_internal: n_embd = 5120 llama_model_load_internal: n_mult = 256 llama_model_load_internal: n_head = 40 llama_model_load_internal: n_head_kv = 40 llama_model_load_internal: n_layer = 40 llama_model_load_internal: n_rot = 128 llama_model_load_internal: n_gqa = 1 llama_model_load_internal: rnorm_eps = 5.0e-06 llama_model_load_internal: n_ff = 13824 llama_model_load_internal: freq_base = 10000.0 llama_model_load_internal: freq_scale = 1 llama_model_load_internal: ftype = 7 (mostly Q8_0) llama_model_load_internal: model size = 13B llama_model_load_internal: ggml ctx size = 0.11 MB llama_model_load_internal: using CUDA for GPU acceleration ggml_cuda_set_main_device: using device 0 (Tesla P40) as main device llama_model_load_internal: mem required = 698.16 MB (+ 400.00 MB per state) llama_model_load_internal: allocating batch_size x (640 kB + n_ctx x 160 B) = 360 MB VRAM for the scratch buffer llama_model_load_internal: offloading 40 repeating layers to GPU llama_model_load_internal: offloaded 40/43 layers to GPU llama_model_load_internal: total VRAM used: 13218 MB llama_new_context_with_model: kv self size = 400.00 MB system_info: n_threads = 12 / 24 | AVX = 1 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 1 | VSX = 0 | sampling: repeat_last_n = 64, repeat_penalty = 1.100000, presence_penalty = 0.000000, frequency_penalty = 0.000000, top_k = 40, tfs_z = 1.000000, top_p = 0.950000, typical_p = 1.000000, temp = 0.100000, mirostat = 0, mirostat_lr = 0.100000, mirostat_ent = 5.000000 generate: n_ctx = 512, n_batch = 512, n_predict = -1, n_keep = 0 ### Instruction: Create poetry about Mount Fuji in 200 words ### Response: Mount Fuji, a majestic sight Rising high into the morning light Snow-capped peak, a work of art A symbol of Japan's heart Its beauty is beyond compare A sacred mountain, pure and fair In spring, cherry blossoms bloom Summer brings green forests, autumn's hue Winter's snow, a peaceful sight The mountain's grace, a source of pride For Japan, a treasure inside A place of wonder, awe and dreams Where nature's beauty, forever gleams Its majesty, a symbol of strength A reminder of life's fleeting length Let us cherish this gift of nature And honor Mount Fuji's pure creation. [end of text] llama_print_timings: load time = 3770.04 ms llama_print_timings: sample time = 72.51 ms / 174 runs ( 0.42 ms per token, 2399.74 tokens per second) llama_print_timings: prompt eval time = 481.74 ms / 22 tokens ( 21.90 ms per token, 45.67 tokens per second) llama_print_timings: eval time = 11099.33 ms / 173 runs ( 64.16 ms per token, 15.59 tokens per second) llama_print_timings: total time = 11679.77 ms
1GPU
CUDA_VISIBLE_DEVICES=0 ./build/bin/main -m ./models/llama-2-13b-chat.ggmlv3.q8_0.bin --temp 0.1 -p "### Instruction: Create poetry about Mount Fuji in 200 words ### Response:" -ngl 40 -b 512 main: build = 952 (3323112) main: seed = 1691227096 ggml_init_cublas: found 1 CUDA devices: Device 0: Tesla P40, compute capability 6.1 llama.cpp: loading model from ./models/llama-2-13b-chat.ggmlv3.q8_0.bin llama_model_load_internal: format = ggjt v3 (latest) llama_model_load_internal: n_vocab = 32000 llama_model_load_internal: n_ctx = 512 llama_model_load_internal: n_embd = 5120 llama_model_load_internal: n_mult = 256 llama_model_load_internal: n_head = 40 llama_model_load_internal: n_head_kv = 40 llama_model_load_internal: n_layer = 40 llama_model_load_internal: n_rot = 128 llama_model_load_internal: n_gqa = 1 llama_model_load_internal: rnorm_eps = 5.0e-06 llama_model_load_internal: n_ff = 13824 llama_model_load_internal: freq_base = 10000.0 llama_model_load_internal: freq_scale = 1 llama_model_load_internal: ftype = 7 (mostly Q8_0) llama_model_load_internal: model size = 13B llama_model_load_internal: ggml ctx size = 0.11 MB llama_model_load_internal: using CUDA for GPU acceleration llama_model_load_internal: mem required = 698.16 MB (+ 400.00 MB per state) llama_model_load_internal: allocating batch_size x (640 kB + n_ctx x 160 B) = 360 MB VRAM for the scratch buffer llama_model_load_internal: offloading 40 repeating layers to GPU llama_model_load_internal: offloaded 40/43 layers to GPU llama_model_load_internal: total VRAM used: 13218 MB llama_new_context_with_model: kv self size = 400.00 MB system_info: n_threads = 12 / 24 | AVX = 1 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 1 | VSX = 0 | sampling: repeat_last_n = 64, repeat_penalty = 1.100000, presence_penalty = 0.000000, frequency_penalty = 0.000000, top_k = 40, tfs_z = 1.000000, top_p = 0.950000, typical_p = 1.000000, temp = 0.100000, mirostat = 0, mirostat_lr = 0.100000, mirostat_ent = 5.000000 generate: n_ctx = 512, n_batch = 512, n_predict = -1, n_keep = 0 ### Instruction: Create poetry about Mount Fuji in 200 words ### Response: Mount Fuji, a majestic sight Rising high into the sky so bright Snow-capped peak, a work of art A symbol of Japan, a treasure to the heart Its base, a lush green forest Home to wildlife, a peaceful nest The mountain's beauty, a source of pride For the Japanese people, it abides In spring, cherry blossoms bloom A delicate pink, a sweet perfume Summer brings warmth and vibrant hues Autumn leaves, a kaleidoscope of views Winter's snow, a serene landscape Year-round, Mount Fuji's grandeur expands A sacred mountain, a spiritual place Where nature's beauty, grace and pace Bring solace to the soul and heart A journey to Mount Fuji, a work of art. [end of text] llama_print_timings: load time = 5271.96 ms llama_print_timings: sample time = 80.64 ms / 199 runs ( 0.41 ms per token, 2467.67 tokens per second) llama_print_timings: prompt eval time = 596.81 ms / 22 tokens ( 27.13 ms per token, 36.86 tokens per second) llama_print_timings: eval time = 13346.94 ms / 198 runs ( 67.41 ms per token, 14.83 tokens per second) llama_print_timings: total time = 14053.59 ms
CPU
./build/bin/main -m ./models/llama-2-13b-chat.ggmlv3.q8_0.bin --temp 0.1 -p "### Instruction: Create poetry about Mount Fuji in 200 words ### Response:" -ngl 0 -b 512 main: build = 952 (3323112) main: seed = 1691227261 ggml_init_cublas: found 1 CUDA devices: Device 0: Tesla P40, compute capability 6.1 llama.cpp: loading model from ./models/llama-2-13b-chat.ggmlv3.q8_0.bin llama_model_load_internal: format = ggjt v3 (latest) llama_model_load_internal: n_vocab = 32000 llama_model_load_internal: n_ctx = 512 llama_model_load_internal: n_embd = 5120 llama_model_load_internal: n_mult = 256 llama_model_load_internal: n_head = 40 llama_model_load_internal: n_head_kv = 40 llama_model_load_internal: n_layer = 40 llama_model_load_internal: n_rot = 128 llama_model_load_internal: n_gqa = 1 llama_model_load_internal: rnorm_eps = 5.0e-06 llama_model_load_internal: n_ff = 13824 llama_model_load_internal: freq_base = 10000.0 llama_model_load_internal: freq_scale = 1 llama_model_load_internal: ftype = 7 (mostly Q8_0) llama_model_load_internal: model size = 13B llama_model_load_internal: ggml ctx size = 0.11 MB llama_model_load_internal: using CUDA for GPU acceleration llama_model_load_internal: mem required = 13555.97 MB (+ 400.00 MB per state) llama_model_load_internal: offloading 0 repeating layers to GPU llama_model_load_internal: offloaded 0/43 layers to GPU llama_model_load_internal: total VRAM used: 360 MB llama_new_context_with_model: kv self size = 400.00 MB system_info: n_threads = 12 / 24 | AVX = 1 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 1 | VSX = 0 | sampling: repeat_last_n = 64, repeat_penalty = 1.100000, presence_penalty = 0.000000, frequency_penalty = 0.000000, top_k = 40, tfs_z = 1.000000, top_p = 0.950000, typical_p = 1.000000, temp = 0.100000, mirostat = 0, mirostat_lr = 0.100000, mirostat_ent = 5.000000 generate: n_ctx = 512, n_batch = 512, n_predict = -1, n_keep = 0 ### Instruction: Create poetry about Mount Fuji in 200 words ### Response: Mount Fuji, a majestic sight Rising high into the sky so bright Snow-capped peak, a work of art A symbol of Japan, a treasure to the heart Its beauty is beyond compare A sacred mountain, pure and fair In spring, cherry blossoms bloom Summer brings green forests, autumn's hue Winter's snow, a blanket of white The mountain's grace, a source of pride For the Japanese people, it abides A spiritual journey, a pilgrimage too Climbing Mount Fuji, a dream come true The sun sets, the moon glows bright A night of tranquility, pure delight The stars twinkle, like diamonds in the sky Mount Fuji, a wonder why In the morning light, it stands tall A beacon of hope, for one and all A symbol of strength, a source of pride Mount Fuji, forever by our side. [end of text] llama_print_timings: load time = 5799.78 ms llama_print_timings: sample time = 91.96 ms / 224 runs ( 0.41 ms per token, 2435.82 tokens per second) llama_print_timings: prompt eval time = 2474.66 ms / 22 tokens ( 112.48 ms per token, 8.89 tokens per second) llama_print_timings: eval time = 80750.70 ms / 223 runs ( 362.11 ms per token, 2.76 tokens per second) llama_print_timings: total time = 83352.26 ms