🖼️

【無料】GPUがなくてもStable Diffusionで遊びたい!

2023/02/06に公開約14,100字4件のコメント

まえがき

「GPUを持ってないけどAI絵を描いてみたい!」というお悩みを抱えている皆さん、Google Colabで満足していませんか?この記事ではModalを使うことでGoogle Colabより(きっと)優れたAI絵生成体験をGETする方法をご紹介します。

What is Modal?

Modalのランディングページ
イケメンなランディングページだね

みんなが良く知るあの「モーダル」ではありません。名前の由来は謎ですが、「Your end-to-end stack for cloud compute」という謳い文句の通りクラウドコンピューティングツール(?)です。Colabみたいにリモートでプログラムを実行する環境を用意してくれるというわけですね。Colabとは違って対話的ではないのであまり初心者には優しくないですが、Pythonの関数の一部だけをリモートで実行するというような柔軟性があるので汎用性はそこそこ高いと思います。

PRICINGのところを見ると、秒単位(時間単位)のお値段が書いてあるのですが、かなりお安いのがお分かり頂けるかと。しかもアイドル中(サーバーがアクセスされていない時間など)の料金はかからないってんだから素晴らしいですね。使うしかない。

Modalの値段リスト

これを見て「あれ?無料やないやんけ」と思ったかもしれません。しかし(少なくとも今は)月30ドル分を無料で使わせてくれるのでありがたく無料で動かさせて頂きましょう。

ちなみにPRICINGのところにStable Diffusionを動かすときのお値段の一例が書いてあり、それによれば30ドルで51000枚の画像が生成できるらしいので十分ですね。(実際は画像の生成だけにお金がかかるわけではないのでもう少し減っちゃいますが)

You run stable diffusion on an A10G GPU. This will run for about 1.5 seconds to generate each image, while using 4GB RAM and 1 CPU.

This will cost $0.000458333/image in GPU charges, $0.00008/image in CPU charges, and $0.00004/image in memory charges, adding up to $0.000578333/image in total charges, i.e. $0.587333 per 1,000 images.

使ってみよう!

1. ModalのアカウントをGETする

Modalのサイトにアクセスして「Sign up」のところからアカウント登録をします。

2023/02/05現在はBeta版らしいのでアカウント登録してもすぐには使えず、Waitlistに入れられるっぽいです。ボクが登録した時は1日経たないぐらいで承認されたんですが、今はもう少し遅いかもしれません。ぼっち・ざ・ろっくでも見ながら気長に待ちましょう。

2. Modalを動かしてみる

公式のGetting Startedに沿ってModalを動かしてみます。

1) ModalのPythonクライアントをダウンロード

まずはpipを使ってクライアントソフトをダウンロードしてきます。

pip install modal-client

ダウンロードできたら次のコマンドを使ってアカウントと紐付けて上げましょう。コマンドを実行するとURLが表示されると思うので、そのURLにアクセスして流れに沿って認証していけばAPIトークンが発行されるはずです。

modal token new

2) コードを動かしてみる

次のプログラムをget_started.pyとして保存してください。

import modal

stub = modal.Stub("example-get-started")

# リモート側で動く関数
@stub.function
def square(x):
    print("This code is running on a remote worker!")
    return x**2

# プログラムのローカル側のエントリポイント(プログラムの中で最初に呼び出されるところ)
@stub.local_entrypoint
def main():
    # Modalの関数は`<function_name>.call`で呼び出す必要がある
    print("the square is", square.call(42))

保存出来たら次のコマンドでプログラムを動かしてみましょう。

modal run get_started.py

いろいろセットアップがなされたあとにthe square is 1764と表示されていれば成功です!

3. Secretsの設定

Hugging Faceからモデルをダウンロードする関係でHugging FaceのAccess Tokenが必要になります。

アカウント登録したら右上のユーザーアイコンをクリックして「Settings」を開きましょう。

Hugging Faceのメニューのドロップダウンリスト

「Access Token」というタブを開いて、「New Token」から新しいAccess Tokenを生成しましょう。

Hugging Faceの設定画面のメニュー一覧

無事Access Tokenが生成できたらそれをコピーして、ModalのGetting Startedに戻り、「Set up integrations/secrets」のところから「Hugging Face」を選択します。そしてVALUEにさっきコピーしたAccess Tokenを貼り付けて、「NEXT」をクリックします。

名前はデフォルトのmy-huggingface-secretのままでOKで、「CREATE」をクリックすると無事Secretsが作成されます。

ModalのSet up integrations/secretsのオプション一覧

これで下準備は終わりです!!

4. Modalを使ってStable Diffusion WebUIを動かす

準備が出来たところで早速Stable Diffusionを動かして行きましょう!まずは以下のプログラムをstable-diffusion-webui.pyという名前で保存してください。

from colorama import Fore
from pathlib import Path

import modal
import shutil
import subprocess
import sys
import shlex
import os

# modal系の変数の定義
stub = modal.Stub("stable-diffusion-webui")
volume_main = modal.SharedVolume().persist("stable-diffusion-webui-main")

# 色んなパスの定義
webui_dir = "/content/stable-diffusion-webui"
webui_model_dir = webui_dir + "/models/Stable-diffusion/"

# モデルのID
model_ids = [
    {
        "repo_id": "hakurei/waifu-diffusion-v1-4",
        "model_path": "wd-1-4-anime_e1.ckpt",
        "config_file_path": "wd-1-4-anime_e1.yaml",
    },
]


@stub.function(
    image=modal.Image.from_dockerhub("python:3.8-slim")
    .apt_install(
        "git", "libgl1-mesa-dev", "libglib2.0-0", "libsm6", "libxrender1", "libxext6"
    )
    .run_commands(
        "pip install -e git+https://github.com/CompVis/taming-transformers.git@master#egg=taming-transformers"
    )
    .pip_install(
        "blendmodes==2022",
        "transformers==4.25.1",
        "accelerate==0.12.0",
        "basicsr==1.4.2",
        "gfpgan==1.3.8",
        "gradio==3.16.2",
        "numpy==1.23.3",
        "Pillow==9.4.0",
        "realesrgan==0.3.0",
        "torch",
        "omegaconf==2.2.3",
        "pytorch_lightning==1.7.6",
        "scikit-image==0.19.2",
        "fonts",
        "font-roboto",
        "timm==0.6.7",
        "piexif==1.1.3",
        "einops==0.4.1",
        "jsonmerge==1.8.0",
        "clean-fid==0.1.29",
        "resize-right==0.0.2",
        "torchdiffeq==0.2.3",
        "kornia==0.6.7",
        "lark==1.1.2",
        "inflection==0.5.1",
        "GitPython==3.1.27",
        "torchsde==0.2.5",
        "safetensors==0.2.7",
        "httpcore<=0.15",
        "tensorboard==2.9.1",
        "taming-transformers==0.0.1",
        "clip",
        "xformers",
        "test-tube",
        "diffusers",
        "invisible-watermark",
        "pyngrok",
        "xformers==0.0.16rc425",
        "gdown",
        "huggingface_hub",
        "colorama",
    )
    .pip_install("git+https://github.com/mlfoundations/open_clip.git@bb6e834e9c70d9c27d0dc3ecedeebeaeb1ffad6b"),
    secret=modal.Secret.from_name("my-huggingface-secret"),
    shared_volumes={webui_dir: volume_main},
    gpu="a10g",
    timeout=6000,
)
async def run_stable_diffusion_webui():
    print(Fore.CYAN + "\n---------- セットアップ開始 ----------\n")

    webui_dir_path = Path(webui_model_dir)
    if not webui_dir_path.exists():
        subprocess.run(f"git clone -b v2.0 https://github.com/camenduru/stable-diffusion-webui {webui_dir}", shell=True)

    # Hugging faceからファイルをダウンロードしてくる関数
    def download_hf_file(repo_id, filename):
        from huggingface_hub import hf_hub_download

        download_dir = hf_hub_download(repo_id=repo_id, filename=filename)
        return download_dir


    for model_id in model_ids:
        print(Fore.GREEN + model_id["repo_id"] + "のセットアップを開始します...")

        if not Path(webui_model_dir + model_id["model_path"]).exists():
            # モデルのダウンロード&コピー
            model_downloaded_dir = download_hf_file(
                model_id["repo_id"],
                model_id["model_path"],
            )
            shutil.copy(model_downloaded_dir, webui_model_dir + model_id["model_path"])

        if "config_file_path" not in model_id:
          continue

        if not Path(webui_model_dir + model_id["config_file_path"]).exists():
            # コンフィグのダウンロード&コピー
            config_downloaded_dir = download_hf_file(
                model_id["repo_id"], model_id["config_file_path"]
            )
            shutil.copy(
                config_downloaded_dir, webui_model_dir + model_id["config_file_path"]
            )

        print(Fore.GREEN + model_id["repo_id"] + "のセットアップが完了しました!")

    print(Fore.CYAN + "\n---------- セットアップ完了 ----------\n")

    # WebUIを起動
    sys.path.append(webui_dir)
    sys.argv += shlex.split("--skip-install --xformers")
    os.chdir(webui_dir)
    from launch import start, prepare_environment

    prepare_environment()
    # 最初のargumentは無視されるので注意
    sys.argv = shlex.split("--a --gradio-debug --share --xformers")
    start()


@stub.local_entrypoint
def main():
    run_stable_diffusion_webui.call()

プログラムの解説は長いのでアコーディオンにしておきます。気になる人はどうぞ。

プログラムの解説

全部解説すると長いので要点だけかいつまんで解説します。分からないところがあったら遠慮なくコメントで質問してください!

モデルの設定

Hugging Faceからダウンロードしてくるモデルを指定しています。repo_idにHugging Face内のリポジトリのIDを、model_pathにモデルのパスを、config_file_pathにコンフィグファイルのパスを指定します。

ただ内部的にはmodel_pathconfig_file_pathもWebUIのmodels/Stable-diffusionディレクトリにコピーされるだけなので気分で分けてるだけです。config_file_pathは省略可能です。

お好きなモデルを追加して遊んでみてください

# モデルのID
model_ids = [
    {
        "repo_id": "hakurei/waifu-diffusion-v1-4",
        "model_path": "wd-1-4-anime_e1.ckpt",
        "config_file_path": "wd-1-4-anime_e1.yaml",
    },
]

関数の設定

@stub.function(...)の()の中にリモート側の設定を書いていくことが出来て、ここでは以下のような指定をしています。

プロパティ 説明
image コンテナイメージの指定。コンテナは一度作成すると使いまわせるのでここで必要なものを事前に全て入れておいて起動時間の短縮を図っている
secret ModalのSecretをここで環境変数に読み込んでいる
shared_volumes {<remote_path>: SharedVolume}のように指定することで、リモート側のパスの一部を永続化(次回起動時にも保存されるように)している
gpu これを指定することでこの関数内でGPUを使えるようになる
timeout 関数の連続実行時間の上限。適当に100分にしているので100分経つとWebUIが落ちる
@stub.function(
    image=modal.Image.from_dockerhub("python:3.8-slim")...,
    secret=modal.Secret.from_name("my-huggingface-secret"),
    shared_volumes={webui_dir: volume_main},
    gpu="a10g",
    timeout=6000,
)

ちなみにSharedVolumeの初期化は次のようにやります。"unique_key"が同じなら他のプログラムからでも同じSharedVolumeにアクセスできます。

volume_main = modal.SharedVolume().persist("unique_key")

セットアップ

まずStable Diffusion WebUIをGitHubからクローンしてきます。SharedVolume内に既にある場合はスキップします。

(AUTOMATIC1111氏のリポジトリから直接クローンしないのはバージョンに依らず動作を安定させるためです。)

webui_dir_path = Path(webui_model_dir)
if not webui_dir_path.exists():
    subprocess.run(f"git clone -b v2.0 https://github.com/camenduru/stable-diffusion-webui {webui_dir}", shell=True)

次にモデルをダウンロードしてきます。SharedVolume内に既にある場合はスキップします。

for model_id in model_ids:
    print(Fore.GREEN + model_id["repo_id"] + "のセットアップを開始します...")

    if not Path(webui_model_dir + model_id["model_path"]).exists():
        # モデルのダウンロード&コピー
        model_downloaded_dir = download_hf_file(
            model_id["repo_id"],
            model_id["model_path"],
        )
        shutil.copy(model_downloaded_dir, webui_model_dir + model_id["model_path"])

    if "config_file_path" not in model_id:
      continue

    if not Path(webui_model_dir + model_id["config_file_path"]).exists():
        # コンフィグのダウンロード&コピー
        config_downloaded_dir = download_hf_file(
            model_id["repo_id"], model_id["config_file_path"]
        )
        shutil.copy(
            config_downloaded_dir, webui_model_dir + model_id["config_file_path"]
        )

    print(Fore.GREEN + model_id["repo_id"] + "のセットアップが完了しました!")

モデルや本体をSharedVolumeに全部入れることと、事前にコンテナ内でセットアップを終えておくことでWebUI起動までの時間を極限まで短縮しています。

WebUI起動

通常はシェルなどでlaunch.pyを実行するのですが、それだと上手く出力が出てくれなかったのでlaunch.pyをインポートしてきて中の関数を実行するというちょっと強引な手段で実行しています。

# WebUIを起動
sys.path.append(webui_dir)
sys.argv += shlex.split("--skip-install --xformers")
os.chdir(webui_dir)
from launch import start, prepare_environment

prepare_environment()
# 最初のargumentは無視されるので注意
sys.argv = shlex.split("--a --gradio-debug --share --xformers")
start()

保存出来たら以下のコマンドで実行します

modal run stable-diffusion-webui.py

初回はいろいろセットアップをする必要があるので結構時間がかかります。ぼっち・ざ・ろっくでも見ながら気長に待ちましょう(2回目)。

Launching Web UI with arguments: ...みたいなのが出てきたら起動が上手く言っている証拠です。ただ起動にもそこそこ時間がかかるのでぼっち・ざ・ろっくでも見て(ry

起動が終わるとWebUIにアクセスするためのURLを表示してくれるはずです。リモートで動かしてる関係で--shareオプションを使ってGradio経由でアクセスすることになります。ローカルのURLに続いて表示されたURLにアクセスしてみましょう。

こんな感じのWebUIの画面が出たら成功です!


親の顔より見た(?)WebUIの画面

WebUIの使い方に関しては他の人が素晴らしい記事をたくさん書いてくれてると思うのでそちらを参考にして頂ければと思います。

https://intindex.stars.ne.jp/archives/12180

5. オマケ: 生成した画像を一括でダウンロードする

以下のプログラムをdownload-output.pyという名前で保存して、modal run download-output.pyとすると./outputsフォルダに生成された画像がダウンロードされます。

自動で差分を取ってダウンロードしてくれるので、毎回同じファイルをダウンロードし直すことはありません。流石だね。

プログラム
import os
import modal
import subprocess
from concurrent import futures

stub = modal.Stub("stable-diffusion-webui-download-output")

volume_key = 'stable-diffusion-webui-main'
volume = modal.SharedVolume().persist(volume_key)

webui_dir = "/content/stable-diffusion-webui/"
remote_outputs_dir = 'outputs'
output_dir = "./outputs"


@stub.function(
    shared_volumes={webui_dir: volume},
)
def list_output_image_path(cache: list[str]):
  absolute_remote_outputs_dir = os.path.join(webui_dir, remote_outputs_dir)
  image_path_list = []
  for root, dirs, files in os.walk(top=absolute_remote_outputs_dir):
    for file in files:
      if not file.lower().endswith(('.png', '.jpg', '.jpeg')):
        continue

      absolutefilePath = os.path.join(root, file)
      relativeFilePath = absolutefilePath[(len(absolute_remote_outputs_dir)) :]
      if not relativeFilePath in cache:
        image_path_list.append(relativeFilePath.lstrip('/'))
  return image_path_list

def download_image_using_modal(image_path: str):
  download_dest = os.path.dirname(os.path.join(output_dir, image_path))
  os.makedirs(download_dest, exist_ok=True)
  subprocess.run(f'modal volume get {volume_key} {os.path.join(remote_outputs_dir, image_path)} {download_dest}', shell=True)

@stub.local_entrypoint
def main():
  cache = []

  for root, dirs, files in os.walk(top=output_dir):
    for file in files:
      relativeFilePath = os.path.join(root, file)[len(output_dir) :]
      cache.append(relativeFilePath)

  image_path_list = list_output_image_path.call(cache)

  print(f'\n{len(image_path_list)}ファイルのダウンロードを行います\n')

  future_list = []
  with futures.ThreadPoolExecutor(max_workers=10) as executor:
    for image_path in image_path_list:
        future = executor.submit(download_image_using_modal, image_path=image_path)
        future_list.append(future)
    _ = futures.as_completed(fs=future_list)

  print(f'\nダウンロードが完了しました\n')

結局Modalで優れたAI絵生成体験が得られるのかという話ですが、ボク的には一長一短という感じです。まとめてみるとこんな感じ。

Modal Colab
コスト 30ドルまで無料 ずっと無料 (利用制限あり)
生成速度 (20steps) 2sぐらい 4sぐらい
起動速度 かなり速い 少し遅い
画像の管理 楽 (差分ダウンロードとかができる) ちょっと大変 (毎回zipにしてダウンロードする必要がある)
難易度 ちょっと難しい 簡単

Modalの方が2倍ぐらい早いので使う価値はあるかなという感じですね。自分に合った方を使ってみてください!

(正直自前のGPUでやる方がカスタマイズ性もやりやすさも段違いなのでそれに越したことはないですボクもGPU欲しい)

参考にした記事

https://fls.hatenablog.com/entry/2023/01/09/110757

GitHubで編集を提案

Discussion

記事ありがとうございます!!

一つ疑問なのですが、stable diffusion websiteを実行している間は常時CPU、メモリ使用料は発生するのでしょうか?
画像生成の計算時にそれに追加して、計算時間分のGPU使用料が加算されるのでしょうか?

質問ありがとうございます~!

ちょっと検証してみた感じ、CPUはあまり使ってないっぽいですがGPUは常に食ってるっぽいですね~たぶん実際には使ってないと思うんですが...

あとはメモリ(RAM)を食うのでその分は料金かかっちゃいますね~

使わない間は落としておいた方が良さげです (当たり前)

検証ありがとうございます!!
常時起動は現実的じゃないですねw
GPU処理だけをModalで行って、WebGUIはローカルで実行する便利なUIツールがあれば良いですが...

そうですね...Modalは関数ごとにリモートで実行するものとローカルで実行するものを分けられるのでWebUI部分だけをローカルに切り出してきたバージョンを用意すれば行けるのかな...?という気がします  ただそうなるとWebUIを解体して再構成するという作業が必要になりますが...

ログインするとコメントできます