画像生成AIモデルを使ってGPUの速度を比べてみた

2024年03月04日 月曜日


【この記事を書いた人】
とみ(とみーとも言う)

地方拠点の一つ、九州支社に所属しています。サーバ・ストレージを中心としたSI業務に携わってましたが、現在は技術探索・深堀業務を中心に対応しています。 2018年に難病を患ったことにより、定期的に入退院を繰り返しつつ、2023年には男性更年期障害の発症をきっかけに、トランスジェンダーとしての道を歩み始めてます。

「画像生成AIモデルを使ってGPUの速度を比べてみた」のイメージ

我が課に検証マシンが入りまして

それまでGeForce RTX 3060で細々とやってた検証が少しばかりやれることが増えてきまして。
そしたらよりやれることを増やしたくなるのがエンヂニヤの性・・・・・・・・・ってやつではないでせうか?

メーカー製の製品をおとなしく使う・・・・訳がなく、自前だったり社内で暇してるGPU等をかき集めたりしながら、色々検証内容としてやれることを増やしていこうとしていた時の話です。

デバイスもそこそこに増えてきて、まぁやっぱり色々検証してみたいんですね、これが。
そこで、やってみようという事で始めたのが、各GPUデバイスの速度測定と比較をしようと。

コンセプト

私の基本思想はこれです。

 こまけぇこたぁ
    いいんだよ!!
  /)
 // /)
`///  __
|イ二つ/⌒⌒\
| 二⊃ (●) (●) \
/  ノ/⌒(_人_)⌒  \
\_/|  |┬|   |
 / \  `ー′     /

なので、精度とかとりあえずおいといて、まずはやりたかったことをやってみて(そのうちの一つが前の記事で取り上げたBLIPの話ですね)、そこで何がつかみ取れるか、そこで行われた動きを理解して、今後の取り組みにどう生かすか?という事をやる、何か面白いことがつかめたらラッキー、つかめなければ次気になるネタを探せ探せーってやる感じですね。

なので、とりあえずなんか共通した手順のもと、色んなGPUで速度測定をしようという事にしました。
そこで思いついたのがStable Diffusionによる画像生成処理です。
Stable Diffusionが割といろんな人に使用されているものであり、パフォーマンスとして直感的に分かりやすいだろうというのが決定ポイントになりました。

ただ、旧世代SDと新世代のSDXLとではモデルサイズが違い過ぎて、旧世代SDだけだと高性能系GPUは一瞬で処理が終わるし、SDXLだけでやろうとすると旧世代のGeForceはメモリが足りなさ過ぎてどうにも動かない。VRAM容量によって一部部門を割って計測しようという方向で進めてみました。

「遊ぶのに、こまけぇことは、いいんだよ」・・・あ、魂の言葉が漏れちゃった・・

設定条件

  • StableDiffusion1.5モデルとして、Huggingfaceの’runwayml/stable-diffusion-v1-5’を使用
  • StableDiffusionXLモデルとして、Huggingfaceの’stabilityai/stable-diffusion-xl-base-1.0’を使用
  • いずれもDiffusionPipeline.from_pretrainedにて実行。torch.float16を使用して実行(FP16モード)
  • stepは50に設定
  • プロンプトはa beautiful view of an isolated lake house in an island に設定
  • スケジューラはEulerスケジューラを使用
  • その他はデフォルト設定に従う。出力解像度はSD1.5だと512×512、SDXLだと768×768。
  • xformerを使用し、速度高速化・メモリ使用率削減を狙う(低スペックのGPUでも処理ができるように・・)
  • enable_attention_slicingに設定し、メモリ使用率削減を狙う(上位GPUは少し速度が落ちる)

所有ハードウェア

  • 検証機器-1:我らが課所属の検証マシン
    • 機種:Supermicro SYS-531A-I
    • CPU :Intel Xeon w3-2423
    • RAM :DDR5 64GB ECC
    • GPU :NVIDIA L4 + NVIDIA Tesla P100, NVIDIA Tesla M60 + NVIDIA GeForce RTX3060/12G
  • 検証機器-2: 我が家のメインデスクトップPC
    • 機種:不明の自作マシン
    • CPU :Intel Xeon E5-1640
    • RAM :DDR4 40GB ECC-Reg
    • GPU :NVIDIA GeForce RTX 2060 Super, NVIDIA GeForce GTX 1050 Ti

それ以外のGPUデバイスは、AzureVMのGPUモデルを使用することにしました。
特にNVIDIA V100搭載VMのクォータ申請がなかなか通らず、少々骨が折れました。

ちょうど私が持ってる最も古いGPU世代が「Maxwell2.0」が搭載された「Tesla M60」ですから、ここから今手持ちの最新世代である「Ada Lovelace」までの速度差を見てみようじゃぁないかという事で。

NVIDIA GPUのコア世代は、以下のような並びとなってます。
Maxwellが登場してたのは2015年ごろでして、気づけばそっからもう十年経過しつつあるんですねー・・
時の流れって早いなぁ・・って思いました。

古 < Maxwell < Pascal < Volta < Turing < Ampere < Hopper < Ada Lovelace < 新

サンプルプログラム

サンプルプログラムは以下の通りなのですが、実はこれ、GPU並列稼働テストを行った時の残骸です。
GPUの並列動作について調べている際に使ってたプログラムをそのまま流用しています。

import warnings
warnings.filterwarnings("ignore")
 
from PIL import Image
import time
from transformers import pipeline
from diffusers import DiffusionPipeline
from accelerate.state import PartialState # using multiple GPUs
import torch
 
#モデル名指定
class CFG:
    diffusion_model_name = 'runwayml/stable-diffusion-v1-5'
 
#pipeline作成
diffusion_pipeline = DiffusionPipeline.from_pretrained(
        CFG.diffusion_model_name,
        torch_dtype=torch.float16
        )
 
#メモリ利用効率化設定の有効化
diffusion_pipeline.enable_xformers_memory_efficient_attention()
diffusion_pipeline.enable_attention_slicing()
 
#SDを使用した画像生成処理本体
def diffusion(prompt, pipeline = diffusion_pipeline):
 
    ### using multiple GPUs
    distributed_state = PartialState()
 
    ### モデルを各GPUに送る
    pipeline.to(distributed_state.device)
 
    ### タスクを分散して処理する
    with distributed_state.split_between_processes(prompt) as prompt:
        result = pipeline(prompt)
        generated_image = result.images[0]
        generated_image.save(f"./{distributed_state.process_index}.png", format='PNG')
 
    return generated_image
 
#Diffusion処理のラッパー
def generate(prompt):
 
    start = time.time()
 
    try:
        output = diffusion(prompt)
        end = time.time()
        time_elapsed = int( round( (end - start), 0) )
        time_elapsed_str = f'{str(time_elapsed)} s'
 
        return output, time_elapsed_str
 
    except Exception as e:
        print('Error:', e)
        return None, e
 
### testing function
prompt = "a beautiful view of an isolated lake house in an island"
 
generated_image, time_ = generate(prompt)
print(time_)

 

結果発表

そしたら、凄く興味深い結果が出たのですよ。

まずこちらがSD1.5部門の結果。早い順に掲示しています。

■SD15
NVIDIA Tesla V100-PCIE-16GB Volta           [00:06<00:00,  8.11it/s]※Azure
NVIDIA GeForce RTX 2060     Turing          [00:07<00:00,  6.48it/s]※個人手持ち
NVIDIA Tesla M60            Maxwell         [00:37<00:00,  1.35it/s]※個人手持ち
NVIDIA GeForce GTX 1050 Ti  Pascal          [01:22<00:00,  1.64s/it]※個人手持ち

そしてこちらがSDXL部門の結果。こちらも順序は同様です。

■SDXL
NVIDIA A100 80GB PCI-e      Ampere          [00:10<00:00,  4.64it/s]※Azure
NVIDIA Tesla V100-PCIE-16GB Volta           [00:17<00:00,  2.87it/s]※Azure
NVIDIA GeForce RTX 3060/12G Ampere          [00:31<00:00,  1.58it/s]※手持ち(課所有)
NVIDIA L4                   Ada Lovelace    [00:39<00:00,  1.25it/s]※実装品
NVIDIA T4                   Turing          [00:46<00:00,  1.08it/s]※Azure
NVIDIA Tesla P100           Pascal          [01:37<00:00,  1.96s/it]※手持ち
NVIDIA Tesla M60            Maxwell         [04:22<00:00,  5.26s/it]※手持ち

共通点として見えるのは、

  • Pascal以前は単位がほぼs/itとなっている
  • Volta以降は単位が全てit/sとなっている

という点ですかね。時間数を見ると分かるのですが、この単位は「it」がiteration(処理回数)を、「s」がseconds(秒数)を意味していまして、見たままの通り「it/s」は秒間処理回数を、「s/it」は1回の処理にかかった秒数を示しています。当然前者の方が速度が速いです。つまり、Pascal世代とVolta世代でGPUの処理速度に大きな差が出ているという事が言えるわけですね。

差が如実に出ているところとしては、SDXL部門のNVIDIA Tesla V100とNVIDIA Tesla P100を比較してもらえれば分かりやすいと思います。NVIDIA Tesla P100ってPascal世代のハイエンドモデルで、それまでのGPUとは異なり、初めてHBM2メモリモジュールを搭載してたGPUです。決してエントリークラスとかではないはずなのですが、それにしてもたった1世代での性能差が大きすぎるのですよね。その性能差、ここで得た値だけを比較すると5.7倍になっちゃうんです。これだけの差がどうして生まれたのでしょう?

実はTensorCoreという計算機能が実装された境目がここなんです。Volta世代からなんです。

TensorCoreという計算コア

Volta世代以降で搭載されたTensorCoreというのは、低精度行列演算を高速化するための装置です。低精度行列演算というのは、ディープラーニングならではの特性なんですが、「1つ1つの数値を細かく正しく計算する」のとは真逆の考え方で「多少値は丸められてもいい。それ以上に値の大小とその分布の方が余程大事」って時に威力を発揮します。GPUが得意とする浮動小数点演算(floatと呼ばれたりする)というのは、通常32bitで表現され、32bitデータを用いて演算されます。しかし、ディープラーニングではGPU上のメモリ領域節約であったり、実行速度の追求だったりを目的として低精度演算がよくつかわれます。以前は16bitが主流でしたが、最近では量子化という技術も登場してきて、8bit、4bitという超低精度演算でもOK!というものが割と多く登場してきています。

しかし、低精度演算を想定していない以前のGPUでは、その精度によっては変換処理が発生してそれがオーバーヘッドになり、逆に「確かにメモリにはモデルが収まるけど、処理させたらめっちゃくちゃ遅い」って状況になりかねませんで、それに備えて登場したのがTensorCoreです。

TensorCoreはタダでさえ単純な演算に特化してるGPUのCUDAコア以上に特化度を上げていて、低精度演算時に行われる行列積の能力にウェイトをより置いた機能を持っています。また、低精度値への変換が難しい場合において、複数の低精度な小数の足し算をした形式の値に変換して、低精度演算を可能にするといったような仕組みを実装させ、とにかく「値の分布情報を極力失わずにより速度的な性能を引き上げること」に全力を傾けて計算を行います。

TensorCoreの実力はみてわかる通り

この仕組みを実装した結果は推して知るべしというか、上記結果を見れば明らかですよね。TensorCoreのある・なしだけでそれだけの効果を上げたんだという事を示すことが出来ます。実はNVIDIA Tesla P100も従来32bitデータ型の領域にそのまま16bitデータを突っ込んで処理をさせていたところに、データの配置の仕方等を改良して従来Maxwell2.0コア世代のものよりも2倍高速に演算できるようなっていたんですが、TensorCoreがもたらした効果というのはそれをはるかに凌駕するものであったという事ですね。

このTensorCoreもコア世代が変わるごとに拡張・統廃合を繰り返して今に至っており、アーキテクチャによりできることがだいぶ異なります。

  • Voltaではfloat16データ型に対応
  • Turingでは、INT8/4/1データ型に対応
  • Ampereでは、float64/16, TensorFloat32, bfloat16, INT8/4/1各データ型に対応
  • Hopperでは、INT4/1が除外

このように取り扱うデータ型はその世代ごとに見直しが繰り返されて実装されていまして、当然それに伴い回路の構成も最適化されていくことにより高速化が進められています。NVIDIA社がなかなかこうしたディープラーニングの分野で後れを取らないのって、物量作戦的なアプローチとは言え、より高速化、という考え方を絶え間なく回し続けてきた結果なんじゃないかなあ?という気がしています。

なので、昨今はだいぶヤフオク!においても古いGPUが売りに出されるケースが増えてまいりましたが、この辺りを考慮しながらGPUを選ばないと結構痛い目に遭うんじゃないかなー・・・・・・という気がします。
私は大枚はたいてM60を調達してしまいましたが、あっという間にP100にその座を明け渡すぐらいには処理が遅く、使い物になりませんでした。こうした特性等を見たうえで、個人向け実験台の調達はすることをお勧めします・・

特に、LLMを扱ったりする際にお世話になる flash-attention の機能ですが、あれはAmpere以降のGPUじゃないと動きませんのでご注意を・・・

とみ(とみーとも言う)

2024年03月04日 月曜日

地方拠点の一つ、九州支社に所属しています。サーバ・ストレージを中心としたSI業務に携わってましたが、現在は技術探索・深堀業務を中心に対応しています。 2018年に難病を患ったことにより、定期的に入退院を繰り返しつつ、2023年には男性更年期障害の発症をきっかけに、トランスジェンダーとしての道を歩み始めてます。

Related
関連記事