- OnnxStreamは、512MB RAMのRaspberry Pi Zero 2でStable Diffusion 1.5とSDXL 1.0 Baseを、スワップ追加やディスクへの中間保存なしで実行することを目指す超小型推論ライブラリ
- 推論エンジンと**
WeightsProvider**を分離し、DiskNoCache、DiskPrefetch、カスタムのロード・キャッシュ・プリフェッチをサポート。HTTPから重みを直接受け取り、ディスク書き込みなしで処理することも可能
- SDXL 1.0 BaseはHugging Face Diffusers 0.19.3実装からエクスポートしたONNXファイルを使用し、Raspberry Pi Zero 2では10ステップの画像生成に約11時間かかる
- SDXLのVAEデコーダはFP32で4.4GB RAMを使い、FP16・UINT8だけでは解決が難しいため、重なり合うタイルデコードとブレンディングでメモリを298MBまで削減
- CPU基準でOnnxRuntimeより最大55倍少ないメモリで動作する代わりに0.5〜2倍遅くなる場合があり、バッチサイズ1と一部のONNX演算子中心という制約がある
OnnxStreamの目標と設計
- OnnxStreamは、Stable Diffusion 1.5をRaspberry Pi Zero 2で実行する問題を目標に始まった
- Raspberry Pi Zero 2は512MB RAMを搭載したマイクロコンピュータ
- Stable Diffusionの推奨最小RAM/VRAMは一般に8GB
- 目標条件は、スワップ領域の追加なし、ディスクへの中間結果のオフロードなし
- 一般的な機械学習フレームワークは推論レイテンシやスループットを優先するため、RAM使用量が大きくなりやすい
- OnnxStreamはメモリ使用量の最小化に焦点を当てた、小さく修正しやすい推論ライブラリ
- 中核構造は、推論エンジンとモデル重み提供コンポーネントである**
WeightsProvider**の分離
- 派生クラスがモデルパラメータのロード、キャッシュ、プリフェッチ方式を実装できる
- カスタム
WeightsProviderはHTTPサーバーからデータを直接ダウンロードし、ディスクの読み書きなしで処理可能
- 基本実装は
DiskNoCacheとDiskPrefetch
- CPU基準でOnnxRuntimeより最大55倍少ないメモリを使いながら、0.5〜2倍遅い程度
Stable Diffusion 1.5の実行
- Stable Diffusion 1.5の例では、OnnxStreamを使って異なるVAEデコーダ精度で画像を生成する
- Raspberry Pi Zero 2のRAMに収まらなかったコンポーネントはVAEデコーダだけだった
- 原因はモデル内部のresidual connection、大きなテンソル、大きなconvolution
- 単精度や半精度だけではRaspberry Pi Zero 2のRAMに収まらなかった
- 解決策は8ビット静的量子化だった
- W8A8精度のVAEデコーダ画像はRaspberry Pi Zero 2で生成され、
MAX_SPEEDオプション使用時で約1.5時間かかる
- 比較用のW16A16画像は同じlatentを使ってPCで生成された
Stable Diffusion XL 1.0 Base対応
- OnnxStreamのStable Diffusion例はSDXL 1.0 Baseをサポートし、Refinerは含まない
- ONNXファイルはHugging Face DiffusersライブラリのSDXL 1.0実装からエクスポートしたもので、使用バージョンは0.19.3
- SDXL 1.0はSD 1.5より計算コストがはるかに大きい
- 最大の違いは512x512ではなく1024x1024画像を生成できる点
- 12コアPCと32GB RAMでHugging Face Diffusersを使い10ステップ画像を生成するのに26分かかる
- SDXLの推奨最小VRAMは一般に12GB
- OnnxStreamはSDXL 1.0を300MB未満のRAMで実行できるため、Raspberry Pi Zero 2でも動作する
- スワップ追加なし
- 推論中のディスク書き込みなし
- Raspberry Pi Zero 2での10ステップ画像生成は約11時間かかる
SDXL専用メモリ最適化
- SDXL 1.0にはSD 1.5と同じ最適化セットを適用するが、一部に違いがある
- UNETモデルはRaspberry Pi Zero 2で300MB未満のRAMで実行するため、UINT8動的量子化を使用する
- 量子化対象は大きな中間テンソルの特定のサブセットに限定される
- SDXL 1.0のVAEデコーダはSD 1.5より扱いが難しい
- SDXL 1.0 VAEデコーダはSD 1.5 VAEデコーダより4倍大きい
- OnnxStreamでFP32精度で実行すると4.4GB RAMを使用する
- SD 1.5ではVAEデコーダをUINT8静的量子化し、RAM使用量を260MBまで減らせた
- SDXL 1.0 VAEデコーダはFP16演算でoverflowが発生し、activationの数値範囲が大きいため、UINT8量子化では良質な画像を得にくい
- sdxl-vae-fp16-fixのようなFP16の解決策もあるが、メモリを半分にしても2.2GBで、Raspberry Pi Zero 2にはなお大きい
- 最終的な解決策は、Hugging Face DiffusersのVAEデコーダ実装から着想を得たタイルデコード
- diffusion結果テンソルのshapeは
(1,4,128,128)
- これを
(1,4,32,32) shapeの重なり合うテンソル5x5、合計25個に分割
- 各タイルは左側および上側のタイルと25%重なる
- 各タイルのデコード結果は
(1,3,256,256)テンソルで、最終画像にブレンドされる
- ブレンディングをオフにするとタイル境界が見え、オンにすると最終結果で境界が見えない
- この方式により、SDXL VAEデコーダのRAM使用量は4.4GBから298MBに削減される
サポート機能と依存関係
- OnnxStreamは、メモリ節約型推論に必要な機能を小さくまとめている
- 推論エンジンと
WeightsProviderの分離
DiskNoCache、DiskPrefetch、カスタムWeightsProvider
- attention slicing
- 8ビットunsigned asymmetric percentile動的量子化
- W8A8 unsigned asymmetric percentile静的量子化
- 量子化モデルのキャリブレーション
- FP16サポート
- よく使われる25個のONNX演算子の実装
- 演算は逐次実行されるが、各operatorはマルチスレッドで動作する
- 実装は単一の実装ファイルとヘッダーファイル構成で、
XnnPackクラスでXNNPACK呼び出しをラップする
- 一部の高速化primitiveはXNNPACKに依存する
- MatMul
- Convolution
- element-wise Add/Sub/Mul/Div
- Sigmoid
- Softmax
性能比較と制約
- Stable Diffusionは3つのモデルで構成される
- text encoder: 672 operations、1億2,300万parameters
- UNET: 2050 operations、8億5,400万parameters
- VAE decoder: 276 operations、4,900万parameters
- バッチサイズ1基準の10ステップ画像生成には、次の実行が必要
- text encoder 2回
- UNET 20回
- VAE decoder 1回
- FP16 UNET基準で、OnnxStreamとOnnxRuntimeのメモリ・時間差は大きい
- OnnxStream: 約0.133GB、18.2〜19.8秒
- OnnxRuntime: 5.085〜7.353GB、7.28〜12.8秒
- OnnxStreamはメモリを最大55倍少なく使うが、0.5〜2倍遅い
- FP32 text encoderはOnnxStreamが0.147GB、OnnxRuntimeが0.641GBを使用
- FP32 VAE decoderはOnnxStreamが1.004GB、OnnxRuntimeが1.330〜2.026GBを使用
- 比較結果にはいくつかの条件が付く
- OnnxRuntimeの初回実行は
InferenceSession再利用前のwarm-up推論
- OnnxStreamはeager設計のためwarm-upの概念はないが、以降の実行ではOSのweightsファイルキャッシュの利点を受けられる
- OnnxStreamは現在、batch size 1ではない入力をサポートしていない
- OnnxRuntimeはUNET実行でbatch size 2を使用し、diffusion全体を大きく高速化できる
- OnnxRuntimeの
SessionOptionsのうちEnableCpuMemArena、ExecutionModeの変更は、テストで有意な差を生まなかった
- NCNNはメモリ使用量と推論時間の面でOnnxRuntimeと非常によく似ていた
- テスト環境はWindows Server 2019、16GB RAM、8750H CPU AVX2、970 EVO Plus SSD、VMWare 8 virtual cores
Attention slicingと量子化
- UNET実行時のattention slicingとVAEデコーダのW8A8量子化は、Raspberry Pi Zero 2で実行可能な水準までメモリを下げるうえで重要だった
- attention slicingはmulti-head attentionでscaled dot-product attentionを計算する際、
Q @ K^T行列全体をmaterializeしないようにする
- UNETモデルのattention head countが8のとき、テンソルshapeは次のとおり
Q: (8,4096,40)
K^T: (8,40,4096)
- 最初のMatMul結果:
(8,4096,4096)
- FP32精度では512MBテンソルになる
- 解決方法は、
Qを縦方向に分割し、各chunkに対してattention演算を実行すること
Q_sliced shapeは(1,x,40)
xは4096をonnxstream::Model::m_attention_fused_ops_partsで割った値
- デフォルト値は2で、カスタマイズ可能
- この方式はFP32 UNETモデル全体のメモリ使用量を1.1GBから300MBに下げる
- FlashAttentionはより効率的な代替になり得るが、AVX・NEONなど対応アーキテクチャ別のカスタムカーネルを書く必要があり、その場合XNNPACKを迂回する必要がある
モデル変換と実行方式
- OnnxStreamは
path_to_model_folder/model.txtで定義されたモデルを実行する
- すべてのモデル演算はASCII形式の
model.txtに入っている
- weightsファイルは同じフォルダの
.binファイル群として存在する必要がある
Modelオブジェクトには複数の選択パラメータを設定できる
- 別のweights providerの指定
- 量子化モデルのactivation clipping rangeファイルの読み書き
- モデルキャリブレーションモード
- FP16演算の使用
- UINT8演算の使用
- UINT8動的量子化の使用
- attention slicingの有効化
- attention分割数の設定
- ONNXファイルをOnnxStreamで使うには、
onnx2txt.ipynbノートブックでmodel.txtと.bin weightsファイルをエクスポートする
- PyTorch
nn.ModuleをONNXにエクスポートする際はいくつかの条件がある
torch.onnx.export呼び出し時、dynamic_axesは空にする必要がある
- OnnxStreamは動的shape入力をサポートしない
- 変換前にONNX Simplifierを実行することが強く推奨される
ビルドと実行準備
- Linux、Mac、Windows、TermuxでStable Diffusion例をビルドできる
- Windowsでは
Visual Studio Toolsのx64 Native Tools Command Promptを使用
- Macでは
brew install cmakeでcmakeのインストールが必要
- まずXNNPACKをビルドする必要がある
- XNNPACK関数prototypeはいつでも変わり得るため、特定時点に合うcommitへcheckoutする手順が含まれる
- 基準時点は
2023-06-27 00:00以前のmaster commit
- その後OnnxStreamリポジトリをcloneし、
src/buildでcmakeによりビルドする
MAX_SPEED=ON
XNNPACK_DIRでXNNPACK cloneパスを指定
MAX_SPEEDオプションは性能を高められるが、ビルド時により多くのメモリを使用する
- Windowsでは約**10%**の性能向上
- Raspberry Piでは50%以上の性能向上
- 生成された実行ファイルが動作しない場合があり、Termuxテストで問題があった
- 問題があれば、まず
MAX_SPEED=OFFに変えることが推奨される
- Stable Diffusion 1.5例のweightsはリポジトリのReleasesから取得でき、サイズは約2GB
- Stable Diffusion XL 1.0 Base weightsはHugging Faceから取得でき、サイズは約8GB
Stable Diffusion例のオプション
- 例の実行ファイルは、モデル選択、入出力、プロンプト、デコード方式をコマンドラインオプションで制御する
--xl: Stable Diffusion 1.5の代わりにStable Diffusion XL 1.0を実行
--models-path: Stable Diffusionモデルフォルダを指定
--ops-printf: 推論中の現在のoperationをstdoutに出力
--output: 出力PNGファイルを指定
--decode-latents: diffusionをスキップし、指定したlatentsファイルをデコード
--prompt: positive promptを指定
--neg-prompt: negative promptを指定
--steps: diffusion step数を指定
--save-latents: diffusion後のlatentsを指定ファイルに保存
- Raspberry Piとデコーダ関連のオプションも別途提供される
--decoder-calibrate: SD 1.5でのみquantized VAE decoderをキャリブレーション
--decoder-fp16: SD 1.5でのみFP16 VAE decoderを使用
--not-tiled: SDXL 1.0でのみtiled VAE decoderを使用しない
--rpi: Raspberry Piで実行するようにモデルを構成
--rpi-lowmem: SDXL 1.0でのみRaspberry Pi Zero 2向け低メモリ構成を適用
1件のコメント
Hacker News のコメント
興味深い。核心はこの一文だと思う: 「OnnxStream は OnnxRuntime よりもメモリ使用量が最大55分の1でありながら、速度低下は0.5〜2倍にとどまる場合がある」
ビデオメモリ/メモリ使用量と推論時間のトレードオフは、Raspberry Pi のようにメモリが限られる場合だけでなく、ほかの状況でも有利に働きそうに見える
実際にこの重みのアンロード方式で、同じメモリ内でより大きなバッチサイズを処理できるなら、レイテンシは増えてもスループットは大きく伸ばせるのではないかと気になる
おそらく「1.5〜2倍遅い」と書きたかったのだと思う
推論はたいてい「このモデルがこのシステムに収まるか」という段階を超えるとメモリ帯域幅に縛られるので、より大きなバッチサイズでスループットを伸ばすうえでは、この手法はあまり役に立たない気がする。インスタンス1つだけでも、すでにメモリコントローラを飽和させている可能性が高い
ただし学習のほうでは役に立つかもしれない
11時間とは、昔 Amiga 500 でレイトレーシングしていた頃を思い出す。「最終」レンダーは間違いなく一晩中回す作業だった
ただしコースティクスは明るく見えても統計的にはかなりまれな現象なので、きちんと得るにはレンダーエンジンの制限を外して、一晩そのまま回す必要がある
出来上がるのは、見事なコースティクスを備えた、腕の足りない作者による平凡なシーン画像1枚だ。本業は続けたほうがよさそうだ
その頃、別の趣味が必要だと思った。ちょうどその直前に、ある優秀な人が OpenGL でシーンを先に見られるようにレンダリングするツールを出していた。Amiga では無理だっただろうが、自分のマシンではぎりぎり動いた
MBP で invoke.ai を使って Stable Diffusion を動かしているのだが、SD のパラメータをもっとうまく詰めるおすすめはあるだろうか? 同じプロンプトと見た目には同じ設定、たとえば Euler A のような同じモデルを使っても、ネットで見かける画像品質にどうしても届かない
標準の SD 1.5 には、速いという理由で Volta を使っている: https://github.com/VoltaML/voltaML-fast-stable-diffusion/com...
本当に良い SD 1.5 の画像品質は、ファインチューニング済みモデル、LoRA、ControlNet、その他の補強機能を惜しみなく使わないと出ない。たとえば元画像を構造としてなぞらせたり、画像内の特定領域ごとにプロンプトを指定したりするようなものだ。InvokeAI も実際には機能が多く、ノード UI の中にこうした補強機能がたくさん隠れているが、Volta のような別の UI はもっと直接的に露出している
一般に、印象的なファインチューニング済みモデルは標準の重みより汎用性がかなり低いが、実用上は大きな問題にならず、結果はかなり良くなることがある
Invoke や Nvidia のグラフィックカードより時間とメモリは多く必要だが、そこまで悪くはない。標準的な 512x768px 品質の画像は 1〜2 s/it、Hires Fix を使った高品質な 1024x1536px 画像は 14〜20 s/it 程度だ
これをデジタルフォトフレームや壁掛け絵に組み込んで動かしたら、本当に格好よさそうだ
https://github.com/rvdveen/epaper-slow-generative-art/
驚くべき成果だが、画像を生成するのには当然ものすごく時間がかかる。README には 11時間と書かれている
例えば Pi Zero 2 を100台、別に W である必要もないが、それで画像100枚を作るのにかかる時間・コスト・必要な全ハードウェア・電力を、平均的なミドルレンジ PC のようなものと比較する、といった具合だ
おそらく PC が依然として勝つ気がする
Zero 2 は実用性というより挑戦課題として使われた感じなので、Pi 4 や 5 のほうがより良い基準点かもしれない
印象的だ
まことに、電灯やトースターでさえ優れた知性を宿す時代が近づいているのだ
この分野を何年も見てきたが、この10年は驚くべきものだった
「だった」と言うのは、最近の 6〜18か月の加速はまたまったく別の次元だからだ
2年後に何ができるかではなく、発展の速度がどれほどさらに速くなるのかが心配だ。そしてまた、さらに、またさらに速くなるのだろう
スタートアップを作ろう
ここまで来ると、Stable Diffusion 系の技術を規制しようとする試みは終わったのではないか?モデルと推論インフラを縮小して PS2 でも動かせるレベルになるなら、全体主義的な監視国家なしにこの技術を止めるのは不可能に見えるし、そういう国家でもかろうじて可能かどうかだ
メディアの「海賊版」やランサムウェアも同じだ
国家は純粋に技術的には執行できないものも、ずっと昔から規制してきた
面白い誇張例だというのは分かるが、PS2 で動かすにはこれよりはるかにさらに小さくする必要がある
Windows 3.1 向けの Stable Diffusion が出る日が待ちきれない
やばい。11時間かかろうが何だろうが、Pi Zero のようなハードウェアで SD が動かせるとはまったく予想していなかった