こんにちは、パレイド技術部です。
前回は 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 / Python | macOS 26.4.x / Python 3.12 |
| PyTorch | 2.10(MPS・bf16 対応) |
| diffusers | ErnieImagePipeline が 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) | |
|---|---|---|
| 公式の推奨 VRAM | 24GB | 12GB |
| 実測 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/it | 130 | 45 |
| denoising 合計 | 6500s (108 分) | 363s (6 分) |
denoising だけ見れば 30 倍速い(s/it 自体も下がっていますが、最大の貢献は単純にステップ数が減ったこと)。
追加した最適化 — VAE tiling が OOM 回避の鍵
Turbo 化と併せて、Mac MPS 環境では次の最適化を入れています:
pipe.vae.enable_tiling()— これが VAE decode の OOM を直接救ったピース。小タイル単位で decode するので VAE のピーク使用量が数百 MB に落ちる。フル版失敗の直接原因だった「denoising 後に 1GB 足りない」問題が消えるpipe.enable_attention_slicing()— diffusers 公式 MPS ドキュメントで RAM 64GB 未満は常時推奨。attention を分割実行してメモリピークを削減。本実験では速度への影響は切り分け計測していないが、メモリ圧抑制には貢献している(後述の「メモリ挙動」セクション参照)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/ にあります。




