Stable Diffusion web UI (AUTOMATIC1111版) 環境を EC2 に構築してみた

# はじめに

「画像生成AIを軽く試してみたいけど手元にゲーミングPC級は無いしグラボは高いし、Google Colab は昨今の事情が…」
と散々悩んだ挙げ句、お試し用に手を出したのが AWS EC2 の GPU インスタンスでした。

ここでは備忘録として、特に EC2 起動後の構築手順を書き連ねます。

# AWS 利用環境

EC2インスタンスの構築手順は割愛します。
CloudFormation で VPC ごと環境を作ってしまえば後片付けも楽々。

## EC2

|項目|設定値|備考|
|:-:|:-:|:-:|
|リージョン|オレゴン(us-west-2)|-|
|インスタンスタイプ|g4dn.xlarge|-|
|料金|USD 0.586/h|(2023/05 現在)|
|vCPU|4|(g4dn.xlarge)|
|メモリ|16GiB|(g4dn.xlarge)|
|インスタンスストア|125GB NVMe SSD|(g4dn.xlarge)|
|EBS|gp3 100GB x1|/dev/sda1|
|AMI|RHEL-9.0.0_HVM-20230313-x86_64-43-Hourly2-GP2||

- g4dn 等の GPU インスタンスを利用するには Service Quotas にて上限緩和申請が必要
    - 「Running On-Demand G and VT instances」に対し、利用したい **vCPU数** を申告する
- 一部 g4dn をサポートしていない AZ(アベイラビリティゾーン) が含まれているので注意
- ElasticIP 等は必要に応じてお好みで

## セキュリティグループ

|ルール|タイプ|プロトコル|ポート範囲|ソース|説明|
|:-:|:-:|:-:|:-:|:-:|:-:|
|インバウンドルール|SSH|tcp|22|0.0.0.0/0|SSH|
|アウトバウンドルール|すべてのトラフィック|すべて|すべて|0.0.0.0/0|-|

- セキュリティグループ名、アウトバウンドの制限などはお好みで
    - models 等のダウンロード用として HTTPS のみ開放する形でも良い
- インバウンドの許可設定は ssh のみ
    - WebUI へのアクセスはポートフォワーディングを利用
    - sshクライアントはお好みで

# 構築手順

EC2 インスタンスを起動し ssh 接続ができたら、以下の流れで環境を整えていきます。

## OS基礎構築(RHEL9)

- パッケージを最新にアップデート
    ```Bash
    sudo dnf -y update
    sudo reboot
    ```
- ssh 再接続後に kernel バージョンを確認
    ```Bash
    uname -a
    ```
    ```:出力
    Linux sdtest 5.14.0-70.49.1.el9_0.x86_64 #1 SMP PREEMPT Thu Feb 23 04:56:09 EST 2023 x86_64 x86_64 x86_64 GNU/Linux
    ```

## GPUドライバ導入(nvidia CUDA)

- GPU搭載確認
    ```Bash
    lspci | grep -i nvidia
    ```
    ```:出力
    00:1e.0 3D controller: NVIDIA Corporation TU104GL [Tesla T4] (rev a1)
    ```
- EPEL9 インストール
    ```Bash
    sudo dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
    ```
- kernel-devel, kernel-headers パッケージ追加
    ```Bash
    sudo dnf -y install kernel-devel-$(uname -r) kernel-headers-$(uname -r)
    ```
- nvidia GPU,CUDA ドライバ インストール
    ```Bash
    distro=rhel9
    arch=x86_64
    sudo dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/$distro/$arch/cuda-$distro.repo
    sudo dnf clean expire-cache
    sudo dnf -y module install nvidia-driver:latest-dkms
    sudo dnf -y install cuda nvidia-gds
    sudo reboot
    ```
- ssh 再接続後に GPU 認識確認
    ```Bash
    nvidia-smi -q | head
    ```
    ```:出力(抜粋)
    ==============NVSMI LOG==============

    Timestamp                                 : Thu May 18 22:31:07 2023
    Driver Version                            : 530.30.02
    CUDA Version                              : 12.1

    Attached GPUs                             : 1
    GPU 00000000:00:1E.0
        Product Name                          : Tesla T4
    ```
- 環境変数設定(/etc/profile.d/cuda.sh)
    ```Bash
    sudo vi /etc/profile.d/cuda.sh
    ```
    ```Bash:/etc/profile.d/cuda.sh
    export CUDA_HOME=/usr/local/cuda
    export PATH=${CUDA_HOME}/bin${PATH:+:${PATH}}
    ```
- 反映
    ```Bash
    source /etc/bashrc
    echo $CUDA_HOME
    ```
    ```Bash:出力
    /usr/local/cuda
    ```

## Python3 導入 (pyenv)

2023/5 現在、Stable Diffusion web UI の推奨は Python 3.10.6 です。
そこで今回は pyenv を利用します。

- 必要パッケージインストール
    ```Bash
    sudo dnf -y install git wget xz-devel sqlite-devel readline-devel libffi-devel bzip2-devel cairo-devel
    ```
    - cairo-devel は ControlNet を利用する場合に必要
- pyenv インストール
    ```Bash
    curl https://pyenv.run | bash
    ```
- ~/.bash_profile に pyenv 利用設定追加
    - 設定追加
        ```Bash
        vi .bash_profile
        ```
        ```Bash:~/.bash_profile
        # 末尾に追加
        export PYENV_ROOT="$HOME/.pyenv"
        command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
        eval "$(pyenv init -)"
        ```
    - 反映
        ```Bash
        source .bash_profile
        pyenv --version
        ```
        ```:出力
        pyenv 2.3.17
        ```
- Python3.10.6 インストール
    ```Bash
    pyenv install 3.10.6
    pyenv global 3.10.6
    python --version
    ```
    ```Bash:出力
    Python 3.10.6
    ```

## Stable Diffusion web-ui インストール

場所は任意ですが、今回はユーザーホームディレクトリ直下に導入します。

- AUTOMATIC1111 版を取得
    ```Bash
    git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
    ```
- webui-user.sh 設定変更
    ```Bash
    cd stable-diffusion-webui/
    vi webui-user.sh
    ```
    ```diff:webui-user.sh
    -#export COMMANDLINE_ARGS=""
    +export COMMANDLINE_ARGS="--opt-channelslast --opt-sdp-attention --no-half-vae"
     
    -#export NO_TCMALLOC="True"
    +export NO_TCMALLOC="True"
    ```
    - xformers は導入しない
        - `--opt-sdp-attention` を用いることで同等またはそれ以上のパフォーマンスが得られる[らしい](https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/8691#discussioncomment-5340883)
        - `--xformers` を利用すると torch(+cu118) と torchvision(+cu117) の間で CUDA バージョンが不一致となり詰む
            - 事前に pip install しても、WebUI 起動時の処理で torchvision が再インストールされてしまい回避できない
- WebUI 起動
    ```Bash
    ./webui.sh
    ```

## swap 設定

インスタンスストアがあるのに使わないのは非常に勿体ないので swap に全振りします。

### 手動での設定

- 未マウントのディスクデバイス(インスタンスストア)があることを確認
    ```Bash
    sudo lsblk -p | grep -v /dev/nvme0
    ```
    ```Bash:出力
    NAME             MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
    /dev/nvme1n1     259:0    0 116.4G  0 disk
    ```
- swap として利用可能にする
    ```Bash
    sudo mkswap /dev/nvme1n1
    sudo swapon /dev/nvme1n1
    free -h
    ```
    ```Bash:出力
                   total        used        free      shared  buff/cache   available
    Mem:            15Gi       589Mi        14Gi       8.0Mi       221Mi        14Gi
    Swap:          116Gi          0B       116Gi
    ```

### OS起動時に自動設定させる

インスタンスストアは揮発性ストレージのため、起動するたびに swap の設定が必要となります。
そこで起動時に swap をマウントするよう設定します。

- nvme-cli インストール
    ```Bash
    sudo dnf -y install nvme-cli
    ```
- swap on/off を制御するスクリプトを作成・設置
    ```Bash
    sudo vi /usr/local/sbin/isswapctl
    ```
    ```Bash:/usr/local/sbin/isswapctl
    #!/bin/bash
    ISDEV=$( /usr/sbin/nvme list | grep 'Instance Storage' | awk '{ print $1 }' )
    if [ -n "$ISDEV" ]; then
        if [ "$1" = "start" ]; then
            /usr/sbin/mkswap $ISDEV && /usr/sbin/swapon $ISDEV
        elif [ "$1" = "stop" ]; then
            /usr/sbin/swapoff $ISDEV
        else
            echo "usage: $0 [start|stop]"
        fi
    else
        echo "Instance Storage device is not found."
    fi
    exit $?
    ```
- 実行パーミッション付与
    ```Bash
    sudo chmod 700 /usr/local/sbin/isswapctl
    ```
    - `sudo isswapctl start` で swap 有効化
    - `sudo isswapctl stop` で swap 無効化
- systemd ユニットファイルを作成
    ```Bash
    sudo vi /etc/systemd/system/isswap.service
    ```
    ```systemd:/etc/systemd/system/isswap.service
    [Unit]
    Description=Use instance store volumes as swap space.
    After=local-fs.target
    ConditionPathExists=/usr/local/sbin

    [Service]
    ExecStart=/usr/local/sbin/isswapctl start
    ExecStop=/usr/local/sbin/isswapctl stop
    Restart=always
    Type=simple
    RemainAfterExit=yes

    [Install]
    WantedBy=multi-user.target
    ```
- systemd isswap.service ユニット設定反映
    ```Bash
    sudo systemctl daemon-reload
    ```
- 動作確認
    - swap 無効状態を確認
        ```Bash
        free -h
        ```
        ```Bash:出力 / free
                       total        used        free      shared  buff/cache   available
        Mem:            15Gi       489Mi        14Gi       8.0Mi       421Mi        14Gi
        Swap:             0B          0B          0B
        ```
    - swap 無効 → 有効化
        ```Bash
        sudo systemctl start isswap
        sudo systemctl status isswap --no-pager
        free -h
        ```
        ```Bash:出力 / systemctl status
        ● isswap.service - Use instance store volumes as swap space.
             Loaded: loaded (/etc/systemd/system/isswap.service; disabled; preset: disabled)
             Active: active (exited) since Sat 2023-05-20 04:23:02 JST; 1min 24s ago
            Process: 1981 ExecStart=/usr/local/sbin/isswapctl start (code=exited, status=0/SUCCESS)
           Main PID: 1981 (code=exited, status=0/SUCCESS)
                CPU: 39ms
        ```
        ```Bash:出力 / free
                       total        used        free      shared  buff/cache   available
        Mem:            15Gi       595Mi        14Gi       8.0Mi       424Mi        14Gi
        Swap:          116Gi          0B       116Gi
        ```
    - swap 有効 → 無効化
        ```Bash
        sudo systemctl stop isswap
        sudo systemctl status isswap --no-pager
        free -h
        ```
        ```Bash:出力 / systemctl status
        ○ isswap.service - Use instance store volumes as swap space.
             Loaded: loaded (/etc/systemd/system/isswap.service; disabled; preset: disabled)
             Active: inactive (dead)
        ```
        ```Bash:出力 / free
               total        used        free      shared  buff/cache   available
        Mem:            15Gi       512Mi        14Gi       8.0Mi       423Mi        14Gi
        Swap:             0B          0B          0B
        ```
- 自動起動設定
    ```Bash
    sudo systemctl start isswap
    sudo systemctl enable isswap
    sudo systemctl is-enabled isswap
    ```
    ```Bash:出力
    enabled
    ```
- 再起動確認
    ```Bash
    sudo reboot
    ```
    - ssh 再接続後 swap が有効になっていることを確認
        ```Bash
        free -h
        ```
        ```Bash:出力
                       total        used        free      shared  buff/cache   available
        Mem:            15Gi       603Mi        14Gi       8.0Mi       183Mi        14Gi
        Swap:          116Gi          0B       116Gi
        ```

## 起動 → webuiアクセス

- sshクライアント側でポートフォワーディングの設定を行い、ssh再接続しておく
    - ローカル(PC)側 7860/tcp → リモートサーバ側 7860/tcp へ転送するよう設定する
    - 設定参考例(Rlogin)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198766/b252fcab-59fd-7716-160d-443a0e5ec0c6.png)
- 起動
    ```Bash
    cd ~/stable-diffusion-webui/
    ./webui.sh
    ```
    ```Bash:出力(抜粋)
    Running on local URL:  http://127.0.0.1:7860
    
    To create a public link, set `share=True` in `launch()`.
    ```
- PCのブラウザで WebUI を開く
    - http://127.0.0.1:7860
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198766/cf48a2e7-6e21-449d-6dfa-6ec15f268a8b.png)

## models等のダウンロードについて

- PCからサーバへアップロードするより、サーバから wget や curl 等で取得するほうが速く済む
    - models などは概ねファイルサイズも大きく時間もそれなりにかかるため
    - 日本 - オレゴン間は通信速度があまり望めない(国内に比べたら **かなり遅い** 部類)

### やっておくと幸せになれるかもしれない設定

- wget にオプションを付与した alias を作成するよう、.bash_profile に設定しておく
    ```Bash:~/.bash_profile
    # 末尾に追加
    alias wgetcd='wget --content-disposition --progress=dot'
    ```
    - `--progress` はお好みで

## 最後に

GPUを搭載したインスタンスは時間あたりの単価も高いので、使い過ぎに注意しましょう。
1ヶ月間フルに使った場合、1ドル140円換算で約60,000円+α(外向きのトラフィック代)くらいかかります。
- 使わない時はインスタンスを停止しておく
- 長期間使わない時は AMI を取得するなどしてからインスタンスは終了(削除)する
- あくまでお試し用やスポットで使うようにする
    - 常用目的なら
        - ゲーミングPCを買ったほうが良い
        - SavingPlans などの長期契約割引を検討する
        - 有料版の Google Colab Pro, Colab Pro+ を検討する
- 請求アラームを設定しておく

ここまで書き連ねてきましたが、実はこれが私にとって「はじめてのQiita」になります。
この投稿内容が少しでも何かのお役に立てれば幸いです。

## 参考文献

- https://dev.classmethod.jp/articles/install-nvidia-cuda-on-ec2-g4-amazon-linux-2/
- https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#network-repo-installation-for-rhel-9-rocky-9
- https://github.com/pyenv/
- https://github.com/AUTOMATIC1111/stable-diffusion-webui
- https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/10331
- https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/8691
- https://blog.serverworks.co.jp/tech/2014/11/28/use-ephemeral-disk-as-swap/