Stable Diffusion を Core ML に変換する

Stable Diffusion も Core ML に変換できる。
MacOSや（実行時間はかかるが）iOSで使うこともできる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/e67760ec-2a9c-9f66-59f7-578d14e6fdd8.png)

# 変換手順

### ml-stable-diffusion をインストール

アップルのstable duffusionのリポジトリをインストールします。

```bash
git clone https://github.com/apple/ml-stable-diffusion.git
cd ml-stable-diffusion
pip3 install -r requirements.txt
pip3 install omegaconf
pip3 install safetensors
```

### case 1: Hugging Face Hub の Stable Diffusion モデルを変換

Hugging Face Hub の model_index.json があるモデルが変換できます。

```bash
python3 -m python_coreml_stable_diffusion.torch2coreml --convert-unet --convert-text-encoder --convert-vae-decoder --convert-safety-checker --model-version <model-version-string-from-hub> -o <output-mlpackages-directory>
```

### case 2: それ以外の色々なモデルを変換 チェックポイント→diffuser→Core ML

世の中には色々なデータで学習した　Stable Diffusion モデルがあります。
これらは多くの場合モデル構造を含んだsafetensorsやckptとしてアップされています。
これらのファイルをdiffuserの形式に変換してから、Core MLに変換します。

##### チェックポイント→diffuser
convert_original_stable_diffusion_to_diffusers.pyという名前のファイルを作り、以下のスクリプトを記述します。
もしくは、[diffusers](https://github.com/huggingface/diffusers/blob/main/scripts/convert_original_stable_diffusion_to_diffusers.py)からダウンロードします。

```python:convert_original_stable_diffusion_to_diffusers.py
# coding=utf-8
# Copyright 2024 The HuggingFace Inc. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Conversion script for the LDM checkpoints. """

import argparse
import importlib

import torch

from diffusers.pipelines.stable_diffusion.convert_from_ckpt import download_from_original_stable_diffusion_ckpt


if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    parser.add_argument(
        "--checkpoint_path", default=None, type=str, required=True, help="Path to the checkpoint to convert."
    )
    # !wget https://raw.githubusercontent.com/CompVis/stable-diffusion/main/configs/stable-diffusion/v1-inference.yaml
    parser.add_argument(
        "--original_config_file",
        default=None,
        type=str,
        help="The YAML config file corresponding to the original architecture.",
    )
    parser.add_argument(
        "--config_files",
        default=None,
        type=str,
        help="The YAML config file corresponding to the architecture.",
    )
    parser.add_argument(
        "--num_in_channels",
        default=None,
        type=int,
        help="The number of input channels. If `None` number of input channels will be automatically inferred.",
    )
    parser.add_argument(
        "--scheduler_type",
        default="pndm",
        type=str,
        help="Type of scheduler to use. Should be one of ['pndm', 'lms', 'ddim', 'euler', 'euler-ancestral', 'dpm']",
    )
    parser.add_argument(
        "--pipeline_type",
        default=None,
        type=str,
        help=(
            "The pipeline type. One of 'FrozenOpenCLIPEmbedder', 'FrozenCLIPEmbedder', 'PaintByExample'"
            ". If `None` pipeline will be automatically inferred."
        ),
    )
    parser.add_argument(
        "--image_size",
        default=None,
        type=int,
        help=(
            "The image size that the model was trained on. Use 512 for Stable Diffusion v1.X and Stable Siffusion v2"
            " Base. Use 768 for Stable Diffusion v2."
        ),
    )
    parser.add_argument(
        "--prediction_type",
        default=None,
        type=str,
        help=(
            "The prediction type that the model was trained on. Use 'epsilon' for Stable Diffusion v1.X and Stable"
            " Diffusion v2 Base. Use 'v_prediction' for Stable Diffusion v2."
        ),
    )
    parser.add_argument(
        "--extract_ema",
        action="store_true",
        help=(
            "Only relevant for checkpoints that have both EMA and non-EMA weights. Whether to extract the EMA weights"
            " or not. Defaults to `False`. Add `--extract_ema` to extract the EMA weights. EMA weights usually yield"
            " higher quality images for inference. Non-EMA weights are usually better to continue fine-tuning."
        ),
    )
    parser.add_argument(
        "--upcast_attention",
        action="store_true",
        help=(
            "Whether the attention computation should always be upcasted. This is necessary when running stable"
            " diffusion 2.1."
        ),
    )
    parser.add_argument(
        "--from_safetensors",
        action="store_true",
        help="If `--checkpoint_path` is in `safetensors` format, load checkpoint with safetensors instead of PyTorch.",
    )
    parser.add_argument(
        "--to_safetensors",
        action="store_true",
        help="Whether to store pipeline in safetensors format or not.",
    )
    parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.")
    parser.add_argument("--device", type=str, help="Device to use (e.g. cpu, cuda:0, cuda:1, etc.)")
    parser.add_argument(
        "--stable_unclip",
        type=str,
        default=None,
        required=False,
        help="Set if this is a stable unCLIP model. One of 'txt2img' or 'img2img'.",
    )
    parser.add_argument(
        "--stable_unclip_prior",
        type=str,
        default=None,
        required=False,
        help="Set if this is a stable unCLIP txt2img model. Selects which prior to use. If `--stable_unclip` is set to `txt2img`, the karlo prior (https://huggingface.co/kakaobrain/karlo-v1-alpha/tree/main/prior) is selected by default.",
    )
    parser.add_argument(
        "--clip_stats_path",
        type=str,
        help="Path to the clip stats file. Only required if the stable unclip model's config specifies `model.params.noise_aug_config.params.clip_stats_path`.",
        required=False,
    )
    parser.add_argument(
        "--controlnet", action="store_true", default=None, help="Set flag if this is a controlnet checkpoint."
    )
    parser.add_argument("--half", action="store_true", help="Save weights in half precision.")
    parser.add_argument(
        "--vae_path",
        type=str,
        default=None,
        required=False,
        help="Set to a path, hub id to an already converted vae to not convert it again.",
    )
    parser.add_argument(
        "--pipeline_class_name",
        type=str,
        default=None,
        required=False,
        help="Specify the pipeline class name",
    )

    args = parser.parse_args()

    if args.pipeline_class_name is not None:
        library = importlib.import_module("diffusers")
        class_obj = getattr(library, args.pipeline_class_name)
        pipeline_class = class_obj
    else:
        pipeline_class = None

    pipe = download_from_original_stable_diffusion_ckpt(
        checkpoint_path_or_dict=args.checkpoint_path,
        original_config_file=args.original_config_file,
        config_files=args.config_files,
        image_size=args.image_size,
        prediction_type=args.prediction_type,
        model_type=args.pipeline_type,
        extract_ema=args.extract_ema,
        scheduler_type=args.scheduler_type,
        num_in_channels=args.num_in_channels,
        upcast_attention=args.upcast_attention,
        from_safetensors=args.from_safetensors,
        device=args.device,
        stable_unclip=args.stable_unclip,
        stable_unclip_prior=args.stable_unclip_prior,
        clip_stats_path=args.clip_stats_path,
        controlnet=args.controlnet,
        vae_path=args.vae_path,
        pipeline_class=pipeline_class,
    )

    if args.half:
        pipe.to(dtype=torch.float16)

    if args.controlnet:
        # only save the controlnet model
        pipe.controlnet.save_pretrained(args.dump_path, safe_serialization=args.to_safetensors)
    else:
        pipe.save_pretrained(args.dump_path, safe_serialization=args.to_safetensors)
```

スクリプトを実行してチェックポイントをdiffuserに変換します。

```bash
python convert_original_stable_diffusion_to_diffusers.py --checkpoint_path <MODEL-NAME>.safetensors --from_safetensors --device cpu --extract_ema --dump_path <MODEL-NAME>_diffusers
```

##### diffuser→Core ML

```bash
python -m python_coreml_stable_diffusion.torch2coreml --convert-vae-decoder --convert-vae-encoder --convert-unet --unet-support-controlnet --convert-text-encoder --model-version <MODEL-NAME>_diffusers --bundle-resources-for-swift-cli --attention-implementation SPLIT_EINSUM -o <MODEL-NAME>_split-einsum && python -m python_coreml_stable_diffusion.torch2coreml --convert-unet --model-version <MODEL-NAME>_diffusers --bundle-resources-for-swift-cli --attention-implementation SPLIT_EINSUM -o <MODEL-NAME>_split-einsum
```

これで、-oに指定したディレクトリにCore MLのmlpackageとResourcesというフォルダが生成されます。
生成されるモデルは、
Resorcesフォルダ内に
TextEncoder.mlmodelcUnet.mlmodelc
VAEEncoder.mlmodelc
VAEDecoder.mlmodelc
merges.txt
vocab.json
です。


##### 変換スクリプトの引数

**--model-version**
HuggingFaceHubのリポジトリ名か、ローカルで生成したdiffuserフォルダの名前を指定できます。

**--quantize-nbits**
指定したビット数（8, 6など）にUnetとTextEncoderを量子化します。
モデルサイズが小さくなり、推論速度が向上します。

**-chunk-unet**
Unetを２つのファイルに分割します。
6ビット以下に量子化していないモデルをiOS/iPadOSで使う際はこの操作が必須です。

**--attention-implementation**
SPLIT_EINSUM　を指定すると、トランスフォーマーをニューラルエンジンに最適化します。iPhone, iPadで使用する場合に推奨。
ORIGINAL はMac用です。

# Swiftでの使用

プロジェクトで使うときは、ResourcesのフォルダをSwiftから読み込んで使います。
（後で量子化する時以外はmlpackageのファイルはいらないと思う）

### パッケージの追加

https://github.com/apple/ml-stable-diffusion

このアップルのリポジトリをSwiftPackageとして追加します。

### モデルの初期化

生成されたResourcesフォルダを Xcodeの Files→ Add files to...からプロジェクトに追加します。

```swift
import StableDiffusion
```

```swift
guard let resourceURL = Bundle.main.url(forResource: "Resources", withExtension: nil) else { return }
do {
    pipeline = try StableDiffusionPipeline(resourcesAt: resourceURL, controlNet: [])
    try pipeline.loadResources()
    
} catch let error {
    print(error)
}
```

### 実行

```swift
var config = StableDiffusionPipeline.Configuration(prompt: "cat")
config.negativePrompt = ""
config.seed = 1

let image = try pipeline.generateImages (configuration: config, progressHandler: { progress in
    print(progress.step)
    return true
}).first
let resultImage = UIImage(cgImage: image!!)
```

# Macで使うのに便利なリポジトリ

以下のリポジトリからmacのサンプルアプリをダウンロードして、変換したモデルをmodelsフォルダに配置するとすぐに使えて便利です。

https://github.com/godly-devotion/MochiDiffusion

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/fcb41203-7f50-71c2-a2cb-7d472a0723e2.png)

# 注釈

変換まではColabなどでもできます。
mlmodelcにコンパイルするにはmacとXcodeのコンパイラが必要です。

🐣
****
フリーランスエンジニアです。
AIについて色々記事を書いていますのでよかったらプロフィールを見てみてください。

もし以下のようなご要望をお持ちでしたらお気軽にご相談ください。
AIサービスを開発したい、ビジネスにAIを組み込んで効率化したい、AIを使ったスマホアプリを開発したい、
ARを使ったアプリケーションを作りたい、スマホアプリを作りたいけどどこに相談したらいいかわからない…

いずれも中間コストを省いたリーズナブルな価格でお請けできます。

お仕事のご相談はこちらまで
rockyshikoku@gmail.com

機械学習やAR技術を使ったアプリケーションを作っています。
機械学習／AR関連の情報を発信しています。

[X](https://twitter.com/JackdeS11)
[Medium](https://medium.com/@rockyshikoku)
[GitHub](https://github.com/john-rocky)