サムネイル自動生成の追試②|ERNIE-Image-Turbo を MacBook Air M5 + diffusers で動かすまで

サムネイル自動生成の追試②|ERNIE-Image-Turbo を MacBook Air M5 + diffusers で動かす — ERNIE-Image, MacBook Air M5, diffusers AIテキスト

こんにちは、パレイド技術部です。

前回は RTX 4070 + ComfyUI で ERNIE-Image-Turbo を動かしました。今回は同じモデルを MacBook Air M5 (32GB 統一メモリ) + diffusers + MPS で動かすまでの工学的な話に絞ります。画質と現行パイプライン (RealVisXL V5.0) との比較は、次回の記事に分けました。

普段のMacBook Air M5 でも動くか

3 月に Windows + ComfyUI環境 から MacBook Air M5 + diffusers に eyecatch パイプラインを移行して以来、普段の背景画像は RealVisXL V5.0 で回しています。SDXL 系の制約として日本語テキスト描画が弱いため、タイトル入りサムネは Pillow で後合成する前提です。

ERNIE-Image はここに直接刺さる候補で、前回 RTX 4070 で実用であることは検証済み。今回は「現行 Mac 環境のまま、バックエンドだけ差し替えて動かせるか」が論点になります。

検証環境

項目
機材MacBook Air M5 (32GB 統一メモリ)
OS / PythonmacOS 26.4.x / Python 3.12
PyTorch2.10(MPS・bf16 対応)
diffusersErnieImagePipeline が main 追加済み、リリース未収録のため git main から install
モデルbaidu/ERNIE-Image-Turbo (約 16GB、DiT 8B の 8 ステップ蒸留版)
比較対象RealVisXL_V5.0 (MPS + diffusers)

最初の壁:MPS の VRAM 上限とメモリ戦略

先に結論から、フル版と Turbo の比較サマリ。

フル版 (50 steps)Turbo (8 steps)
公式の推奨 VRAM24GB12GB
実測 MPS ピーク40.58 GB
(denoising 40GB + VAE +1GB でOOM)
~32 GB 相当
(python RSS 35.95 GB)
Swap 使用(M5 32GB RAM)30 GB
(最適化なし)
25.41 GB
(attention_slicing / vae_tiling 込み)
メモリプレッシャー黄〜赤が交互黄で安定、赤なし
1 枚所要時間108 分 denoising → VAE decode で OOM平均 9 分で完走(初回 5 分、連続バッチは 10 分程度)
結論MacBook Airでは厳しい(denoising は通ってuも VAE decode で転ぶ)実用域。 1 日数枚ならバッチで十分

以降、なぜフル版が落ちたか、Turbo で通すために何をしたか、の順に整理します。

フル版は VAE decode で 1GB 足りずに死ぬ

フル版 8B (50 ステップ) に挑んだ最初の試行は当然ですがあっさり失敗しました。denoising は 50/50 完走したのに、最後の VAE decode で MPS OOM

100%|██████████████| 50/50 [1:48:40<00:00, 130.42s/it]
RuntimeError: MPS backend out of memory
  (MPS allocated: 32.02 GiB, other allocations: 8.56 GiB, max allowed: 42.43 GiB).
  Tried to allocate 1.02 GiB on private pool.

1 時間 48 分の denoising を回し切った直後、あと 1GB だけ足りなくて落ちる。以前、LTX2.2 に挑んだ時もそうでしたが、統一メモリ 32GB の Mac で「24GB VRAM 級」モデルを使うと、こういう最後の一押しで事故ります。メモリをやりくりすれば動く可能性はありますが、AI生成中に裏で何もできなくなるため現実的ではないでしょう。フル版実験はこの記事では紐解かず、Turbo (8 ステップ) に切り替えMPS でそのままフル解像度 1264×848 を通す 方向へ舵を切ります。

24GB 推奨のモデルがなぜ MPS で 40GB を要求するのか

「公式は 24GB VRAM で動くと言っているのに実測 40GB 超」の差分は、CUDA 前提の最適化が MPS では効かないことに由来します。

  • enable_model_cpu_offload() が CUDA 専用。公式 example は pipe.enable_model_cpu_offload() を呼び、text_encoder / pe_enhancer (prompt enhancer) / transformer / VAE を順次 GPU に載せ替えて瞬間ピークを最大モジュール分に抑えます。MPS はこの API が未対応 で、全モデルが常駐したままになる
  • 常駐だけで ~25GB: DiT 8B (bf16 16GB) + Mistral3 text encoder (~6GB) + Ministral-3 pe enhancer (~6GB) + AutoencoderKLFlux2 (~1GB)。ここに denoising の活性化 ~8GB が乗って ~33GB、VAE decode 時さらに +1GB 要求で 40GB 超
  • MPS には flash-attention / SDPA の融合カーネルが無い。CUDA では融合により消される中間テンソルが、MPS では愚直に確保される
  • bf16 の一部演算が内部 fp32 にフォールバック。MPS の bf16 サポートは新しく (PyTorch 2.10 で実用域)、特定の kernel では fp32 昇格が起きてメモリが倍化するケースがある
  • 解像度差: Baidu の想定は 1024×1024 基準。今回は 3:2 比の 1264×848 (pareido サムネ用)。ピクセル数はほぼ同じだが、DiT attention は O(N²) でトークン数に跳ねる

つまり 「24GB VRAM 推奨」は “CUDA + cpu_offload + 1024×1024” の条件付きで、この記事の「MPS + 全モジュール常駐 + 1264×848」には適用できない、というのが正解。MPS で同じモデルを動かすには VRAM 2 倍換算で見積もるのが安全、という運用知見が得られました。

完走できた最大の理由は Turbo への切り替え

フル版から Turbo への変更は denoising ステップ数を 50 → 8 に減らす。重みもパイプライン構造も同じ DiT 8B ですが、ステップ数が 6 倍少ないので同じメモリプレッシャーが 1/6 の時間しかかからない。実測値:

フル版 (50 steps)Turbo (8 steps)
s/it13045
denoising 合計6500s (108 分)363s (6 分)

denoising だけ見れば 30 倍速い(s/it 自体も下がっていますが、最大の貢献は単純にステップ数が減ったこと)。

追加した最適化 — VAE tiling が OOM 回避の鍵

Turbo 化と併せて、Mac MPS 環境では次の最適化を入れています:

  1. pipe.vae.enable_tiling()これが VAE decode の OOM を直接救ったピース。小タイル単位で decode するので VAE のピーク使用量が数百 MB に落ちる。フル版失敗の直接原因だった「denoising 後に 1GB 足りない」問題が消える
  2. pipe.enable_attention_slicing() — diffusers 公式 MPS ドキュメントで RAM 64GB 未満は常時推奨。attention を分割実行してメモリピークを削減。本実験では速度への影響は切り分け計測していないが、メモリ圧抑制には貢献している(後述の「メモリ挙動」セクション参照)
  3. pipe.vae.enable_slicing() — VAE の batch 軸スライシング。batch=1 では効果は限定的、保険として併用

結論: 「動かなかったフル版が動くようになった」最大の要因は Turbo 化 (step 数削減)VAE tiling が OOM を防ぐ最後のピース、attention_slicing は Mac MPS の一般則として入れている、という階層になります。

実装 — 現行DiffusersClientとドロップイン互換の ERNIE ラッパー

現行は DiffusersClient.generate(positive, width, height, ...) を叩く作りです。同じシグネチャで ERNIE を返す ErnieImageClient を書けば、将来バックエンドを切り替えるだけで済みます。

# ernie_backend.py(抜粋)
class ErnieImageClient:
    def __init__(self, model_id="baidu/ERNIE-Image-Turbo",
                 attention_slicing=True, vae_tiling=True, use_pe=False):
        ...

    def _load_pipeline(self):
        from diffusers import ErnieImagePipeline
        import torch
        self._pipe = ErnieImagePipeline.from_pretrained(
            self._model_id, torch_dtype=torch.bfloat16,
        )
        if self._vae_tiling:
            self._pipe.vae.enable_tiling()
            self._pipe.vae.enable_slicing()
        if self._attention_slicing and self._device == "mps":
            self._pipe.enable_attention_slicing()
        self._pipe.to(self._device)

    def generate(self, positive, width=1264, height=848,
                 steps=8, cfg=1.0, seed=-1, **_):
        ...
        return self._pipe(
            prompt=positive, width=width, height=height,
            num_inference_steps=steps, guidance_scale=cfg,
            use_pe=self._use_pe, generator=generator,
        ).images[0]

一般的なチェックポイントとは異なり negative prompt は使いません(ErnieImagePipeline が受け取らない仕様)。また generator は MPS だと CPU 側で作る必要があります(PyTorch 2.10 でも torch.Generator(device="mps") は NGでした)。

まとめ — 動くようにはなった

  • 公式推奨 24GB VRAM のフル版 ERNIE-Image は、MPS では 全モジュール常駐 + cpu_offload 非対応で実効 40GB 級になり、32GB RAM では VAE decode で落ちる
  • Turbo (8 ステップ) への切り替えで denoising が 30 倍速まり、実効メモリも 32GB 範囲に収まる
  • さらに vae.enable_tiling() + enable_attention_slicing() の 2 点を噛ませて VAE decode の OOM を回避、これで Mac M5 単体で 1264×848 が平均 9 分で生成できるようになった
  • 現行 DiffusersClient とドロップイン互換のラッパー ErnieImageClient を書いたので、将来 eyecatch CLI のバックエンド切替は容易

次回(2026-04-22 予定): 6 プロンプトで実際に生成した画像を並べ、現行パイプラインの RealVisXL V5.0 と compare モードで画質・速度を突き合わせます。スポイラーすると 置換メリットは思ったほど単純ではなく、プロンプト経路ごと差し替える必要がある という結論になりました。そのあたりを実画像と一緒に見てもらう予定です。

experiments のコード一式は experiments/ernie_image_eyecatch_mac/ にあります。

タイトルとURLをコピーしました