端の知識の備忘録

技術メモになりきれない、なにものか達の供養先

【続】鯖落ちGPUを使った安価な機械学習用マシンの作り方 ~夢のマルチGPU環境にしてみる編~

!!注意!!: この記事で取ったベンチマークは、片方のGPUのみECC有効となっていたことが判明しました。PCIeのレーン数も双方のGPUで異なっており、非常にアンバランスな条件で測定した結果となっておりますので、マルチGPUの効果を見るのにはあまり適しておりません。後記事にてEPYCを購入し、ちゃんとECCを無効化した上で両GPUともPCIe x16で接続した結果を公開しましたので、こちらも合わせて見ていただければと思います。

前記事:

hashicco.hatenablog.com

後記事 :

hashicco.hatenablog.com

前書き

きっかけは、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.binGPUなし/1GPU/2GPUで動作させて実行時間を測りました。結果、5%ほど速いスピードで推論できることがわかりました。

PCIeの帯域が問題なのか、あまりうまくスケールしていない感じです。これは中古EPYC買うフラグが立ってしまったかもしれません。

とはいえとりあえずマルチGPUの意味は確認できたので、あとは実用的にKaggleコンペなどで使ってメリットを出していきたいと思います。

直近では次のコンペに出てみようと思ってますので、ここでP40x2を有効活用することができれば嬉しいなあ。

www.kaggle.com

セットアップ

すでに前の記事で構築済みのマシンに、お手製ブラケットでファンを取り付けたP40を増設しました。特にOS上は設定変更などいらずに2枚目のGPUが認識されるようになりました。

1. Transformers公式のDDPチュートリアル

準備

こちらのチュートリアルに、TransformersでマルチGPUを使ってトレーニングを行う方法が紹介されています。ここのコードをベンチマークに利用しました。

huggingface.co

ここのチュートリアルのコードを実行するためには、ソースから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を追加しました。

pc.watch.impress.co.jp

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を使う方法に関しては次のページを参照しました。

note.com

次の手順で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