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

2023年5月27日土曜日

stable-diffusion

t f B! P L

自分用のメモ。

  • (2023/06/16)追記: Additional NetworksやControlNetなどよく使いそうな拡張機能を最初から入れたものにコードを差し替え。以前のコードは末尾に移動。
  • (2023-07-28)追記: 1.5.1用に調整。

事前に用意するもの

  • githubのアカウント
  • pythonが動く環境を用意することができる程度の知識
  • 使いたいモデルとVAE

Modalとは

  • クラウド上でpythonのコードを動かすことができるサービス
  • GPUが使える(T4, A10G, A100 20Gb, A100 40Gb)
  • ローカルで実行し、一部分のメソッドをクラウド上で動かすイメージ
  • 毎月$10分の無料クレジットがもらえる従量課金制
  • 起動が早い

なお、今なら1月$30分の無料クレジットがもらえます。

料金

従量課金としては安め。GPUだけでなくCPUやmemoryにも料金が発生しますが、アイドル時には料金が発生しないと書いてあるのがとても良い。

一方でShared volume storage(永続ストレージ)が$2/GiB/monthとなっているのが少し気になりますが、登録にクレジットカードを要求しないのでまずは試してみます。

Modalの使い方

アカウントの取得

https://modal.com/signupに行ってGitHubのアカウントでサインアップします。今はまだベータ版で承認のために、どこでModalを知ったのか、Modalで何をしようとしているか教えてくださいといった内容のメールが届くので、適当に書いて返事します。半日ぐらいで登録したよというメールが来たのでこれでアカウントの取得ができました。

込み具合などでメールが届く時間が変わるかもしれません。

Modalの準備

Getting Startedに従ってライブラリとトークンを取得します。適当な作業用ディレクトリに行き、pythonの仮想環境を作り、そこで作業します。

virtualenv venv
venv\Scripts\activate.bat

modalのライブラリをインストール。

pip install modal-client

Modalで使うトークンを取得。

modal token new

トークンはユーザーディレクトリに保存されます。(例: C\Users\inuneko\.modal.toml)

Stable Diffusion WebUIのインストール

導入の仕方をわかりやすくまとめてくれている記事を参考にインストールします。Shared volumeにリポジトリをクローン、モデルのアップロード、webuiの実行と手順を3つに分けました。

永続ストレージ(Shared volume)を作成してWebUIをgit clone

以下をcloningsdwebui.pyとしてコピペ、保存します。拡張機能が必要ない場合は該当部分を削除してください。

import os
import subprocess

import modal


persist_dir = "/content"

stub = modal.Stub("stable-diffusion-webui")
volume = modal.SharedVolume().persist("sdwebui-volume")

image = modal.Image.debian_slim().apt_install("git")

@stub.function(image=image,
               shared_volumes={persist_dir: volume},
               timeout=1800)
def cloning():
    os.chdir(persist_dir)
    # subprocess.run("git config --global http.postBuffer 200M", shell=True)
    subprocess.run("git clone -b v2.5 https://github.com/camenduru/stable-diffusion-webui", shell=True)

    # download some extensions
    os.chdir(persist_dir + "/stable-diffusion-webui/extensions")
    subprocess.run("git clone https://github.com/kohya-ss/sd-webui-additional-networks", shell=True)
    subprocess.run("git clone https://github.com/Mikubill/sd-webui-controlnet", shell=True)
    subprocess.run("git clone https://github.com/hako-mikan/sd-webui-lora-block-weight", shell=True)
    subprocess.run("git clone https://github.com/hako-mikan/sd-webui-regional-prompter", shell=True)


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

コマンドラインで実行。

modal run cloningsdwebui.py

modal volume ls (volume名) (ディレクトリ)でshared volumeの中身を確認できます。

modal volume ls sdwebui-volume /
Directory listing of '/' in 'sdwebui-volume'
┌────────────────────────┬──────┐
│ filename               │ type │
├────────────────────────┼──────┤
│ stable-diffusion-webui │ dir  │
└────────────────────────┴──────┘

メモ

  • /contentディレクトリにshared volumeの/をマウントするイメージ。クラウド上での操作は/content/stable-diffusion-webuiに対して行います。
  • webuiのバージョンを固定するためにAUTOMATIC1111の本家リポジトリではなくcamenduruのリポジトリを利用しています。ModalはImage内で記述したpip_installなどを実行した後、その環境を保存することで次回以降はスムーズに起動してくれます。このときrequirements.txtに更新があってバージョンの食い違いがあると毎回入れ直しになります。
  • たまにgit cloneに失敗する。そのときはgit config --global http.postBuffer 200Mしてる部分のコメントアウトを解除する。

modelやVAEなどをModalのストレージにアップロード

コマンドラインからmodelやVAEなどをそれぞれアップロードしていきます。大文字小文字は区別されるので注意してください。

modal volume put sdwebui-volume AbyssOrangeMix2_hard_pruned_fp16_with_VAE.safetensors /stable-diffusion-webui/models/Stable-diffusion/AbyssOrangeMix2_hard_pruned_fp16_with_VAE.safetensors

VAE、embeddings、Loraなども同様。送り先のファイル名を省略すると元のファイル名で保存されます。

modal volume put sdwebui-volume C:\path\to\VAE\pr_orangemix.vae.pt /stable-diffusion-webui/models/VAE/

modal volume put sdwebui-volume C:\path\to\embeddings\easynegative.safetensors /stable-diffusion-webui/embeddings/easynegative.safetensors

modal volume put sdwebui-volume C:\path\to\lora\yourfavoritelora.safetensors  /stable-diffusion-webui/models/Lora/

ControlNetで使うモデルはextensions/sd-webui-controlnet/models/に入れます。

modal volume put sdwebui-volume C:\path\to\control_v11p_sd15_openpose.pth /stable-diffusion-webui/extensions/sd-webui-controlnet/models/

実行

以下をrunsdwebui.pyとして保存します。

import os
import sys
import subprocess
import shlex

import modal


persist_dir = "/content"
webui_dir = persist_dir + "/stable-diffusion-webui"

stub = modal.Stub("stable-diffusion-webui")
volume = modal.SharedVolume().persist("sdwebui-volume")


image = (
    modal.Image.from_dockerhub("python:3.10-slim")
    .apt_install("git", "libgl1-mesa-dev", "libglib2.0-0", "libsm6", "libxrender1", "libxext6")
    .pip_install(
        "torch==2.0.1",
        "torchvision==0.15.2",
        extra_index_url="https://download.pytorch.org/whl/cu118"
    )
    .pip_install(
        "GitPython==3.1.30",
        "Pillow==9.5.0",
        "accelerate==0.18.0",
        "basicsr==1.4.2",
        "blendmodes==2022",
        "clean-fid==0.1.35",
        "einops==0.4.1",
        "fastapi==0.94.0",
        "gfpgan==1.3.8",
        "gradio==3.32.0",
        "httpcore==0.15",
        "inflection==0.5.1",
        "jsonmerge==1.8.0",
        "kornia==0.6.7",
        "lark==1.1.2",
        "numpy==1.23.5",
        "omegaconf==2.2.3",
        "open-clip-torch==2.20.0",
        "piexif==1.1.3",
        "psutil==5.9.5",
        "pytorch_lightning==1.9.4",
        "realesrgan==0.3.0",
        "resize-right==0.0.2",
        "safetensors==0.3.1",
        "scikit-image==0.20.0",
        "timm==0.6.7",
        "tomesd==0.1.2",
        "torchdiffeq==0.2.3",
        "torchsde==0.2.5",
        "transformers==4.25.1",
    )
    .pip_install(
        "git+https://github.com/mlfoundations/open_clip.git@bb6e834e9c70d9c27d0dc3ecedeebeaeb1ffad6b",
    )
    .run_commands(
        "python -m pip install --no-deps xformers==0.0.20"
    )
    .pip_install(
        "mediapipe",
        "svglib",
        "fvcore",
        "send2trash~=1.8",
        "dynamicprompts[attentiongrabber,magicprompt]~=0.29.0",
    )
)

@stub.function(image=image,
               shared_volumes={persist_dir: volume},
               gpu="T4",
               timeout=3600)
def run_webui():
    sys.path.append(webui_dir)
    os.chdir(webui_dir)

    subprocess.run("python launch.py --share --xformers --no-half-vae --opt-sdp-no-mem-attention --opt-channelslast --enable-insecure-extension-access", shell=True)


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

コマンドラインで実行。

modal run runsdwebui.py

実行するとhttps://(適当な文字列).gradio.liveというURLが出てくるのでブラウザでアクセスするとStable Diffusion WebUIを操作することができます。

メモ

  • Imageのpip_installでインストールするパッケージのバージョンはrequirements.txtを見て指定しています。
  • テストのためにGPUをT4に、タイムアウトを1時間に設定していますが、動かし方がわかったら(コストと相談して)gpu="T4"のところをgpu="A10G"に変えてより性能の良いGPUにしてみたりtimeout=3600を変更したり取り外したりして調整してください。

WebUIを終了する

遊び終わったらWebUIを止めます。アイドル時には料金が発生しないといっても、使用するリソースがゼロではないので止め忘れるとcreditsがじわじわと消費されていきます。https://modal.com/appsに行って動いているアプリを選びDelete。このときshared volumeを削除しないように注意してください。

仕様なのか私の環境(Windows cmd.exe)だけなのかわかりませんが、Ctrl+Cは効きません。上記のDeleteをしても入力可能にならないのでCtrl+Breakで抜けます。

画像のダウンロード

download_images.py

import os
import shutil

import modal


persist_dir = "/content"
webui_dir = persist_dir + "/stable-diffusion-webui"
outputs_dir = webui_dir + "/outputs"

zipname = "outputs"

stub = modal.Stub("download-images")
volume = modal.SharedVolume().persist("sdwebui-volume")

image = modal.Image.from_dockerhub("python:3.10-slim")


@stub.function(image=image,
               shared_volumes={persist_dir: volume},
               timeout=1800)
def get_imagedata():
    shutil.make_archive(f'/tmp/{zipname}', format='zip', root_dir=outputs_dir)
    f = open(f'/tmp/{zipname}.zip', 'rb')
    return f.read()


@stub.function(image=image,
               shared_volumes={persist_dir: volume},
               timeout=1800)
def clean_outputs():
    shutil.rmtree(outputs_dir)
    os.mkdir(outputs_dir)


@stub.local_entrypoint()
def main():
    import datetime
    dt_now = datetime.datetime.now()
    timestamp = dt_now.strftime('%Y%m%d%H%M%S')

    fileb = get_imagedata.call()
    with open(timestamp + f'{zipname}.zip', 'wb') as f:
        f.write(fileb)

    # outputsディレクトリ以下を削除
    clean_outputs.call()

実行。

modal run download_images.py

outputs以下をzipでまとめてローカルに保存し、全削除しています。

感想

まずModalの理解が必要で、使用感としてはエラー無く動かせるようになるまでが少し大変ですが、一度動くものを用意できるとそれ以降とても早くて楽といった印象です。

まだわからないことがいろいろとあるので、何かあったら追記するかもしれません。

クラウド上でGPUを動かすサービスを選ぶ際に、Modalは選択肢として十分にありえます。

Modalを使ったLoRAの作成方法も書きました

追記: 以前のコード

拡張機能を入れないコードも置いておきます。

メモ: [Bug]: Error after startup #1381によるとControlNetはインストール後に(Apply and restart UIではなく)webui自体の再起動が必要になるらしいです。Modalを動かしたままwebuiだけを終了して再起動するのは難しいので、ControlNetを使いたいときはImageを作る段階で依存ライブラリをpip installしておいて一発で起動できるようにしておく必要があります。

cloningsdwebui.py

import os
import subprocess

import modal


persist_dir = "/content"

stub = modal.Stub("stable-diffusion-webui")
volume = modal.SharedVolume().persist("sdwebui-volume")

image = modal.Image.debian_slim().apt_install("git")

@stub.function(image=image,
               shared_volumes={persist_dir: volume},
               timeout=1800)
def cloning():
    os.chdir(persist_dir)
    # subprocess.run("git config --global http.postBuffer 200M", shell=True)
    subprocess.run("git clone -b v2.3 https://github.com/camenduru/stable-diffusion-webui", shell=True)


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

runsdwebui.py

import os
import sys
import subprocess
import shlex

import modal


persist_dir = "/content"
webui_dir = persist_dir + "/stable-diffusion-webui"

stub = modal.Stub("stable-diffusion-webui")
volume = modal.SharedVolume().persist("sdwebui-volume")


image = (
    modal.Image.from_dockerhub("python:3.10-slim")
    .apt_install("git", "libgl1-mesa-dev", "libglib2.0-0", "libsm6", "libxrender1", "libxext6")
    .pip_install(
        "torch==2.0.1",
        "torchvision==0.15.2",
        extra_index_url="https://download.pytorch.org/whl/cu118"
    )
    .pip_install(
        "blendmodes==2022",
        "transformers==4.25.1",
        "accelerate==0.18.0",
        "basicsr==1.4.2",
        "gfpgan==1.3.8",
        "gradio==3.28.1",
        "numpy==1.23.5",
        "Pillow==9.4.0",
        "realesrgan==0.3.0",
        # "torch",
        "omegaconf==2.2.3",
        "pytorch_lightning==1.9.4",
        "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.30",
        "torchsde==0.2.5",
        "safetensors==0.3.1",
        "httpcore<=0.15",
        "fastapi==0.94.0",
        "clip",
        "test-tube",
        "diffusers",
        "invisible-watermark",
        "gdown",
        "huggingface_hub",
        "colorama",
    )
    .pip_install(
        "git+https://github.com/mlfoundations/open_clip.git@bb6e834e9c70d9c27d0dc3ecedeebeaeb1ffad6b",
    )
)

@stub.function(image=image,
               shared_volumes={persist_dir: volume},
               gpu="T4",
               timeout=3600)
def run_webui():
    sys.path.append(webui_dir)
    os.chdir(webui_dir)
    from launch import start, prepare_environment

    prepare_environment()

    sys.argv = shlex.split("--a --no-half-vae --opt-sdp-no-mem-attention --opt-channelslast --share --enable-insecure-extension-access")
    start()


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

QooQ