技術約18分で読めます

Anima LoRA学習をMacのMPSで動かせた記録

いけさん目次

過去、このブログでは何回かLoRA学習を行ってきたが、Macでの学習は失敗に終わっており、以後はずっとRunPodを使って学習を行っている。
今回、Muonオプティマイザの実装を試す前に、AnimaLoraToolkit + Anima-BaseのLoRA学習が今のMacで実際どこまで動くのかを確かめた。
検索してもAnima系をMPSで学習させた情報はほとんど出てこなかった。

検証環境

項目内容
ベースモデルAnima-Base v1.0(Cosmos-Predict2 2B系)
学習ツールAnimaLoraToolkit(anima_train.py
マシンMacBook Pro(M1 Max, 64GB)
学習設定lora_type: "lora" / rank4 / batch1
データかな単体79枚(3キャラ合体LoRA用セットから抜粋、.txt caption)

学習させるのはこのかなちゃんで、茶髪のサイドポニー・青いシュシュ・あほ毛が特徴になる。

学習データの参照画像(かな、茶髪サイドポニー+青シュシュ+あほ毛)

セットアップで最初に躓いた点

requirements.txt をそのまま pip install -r すると、pillow-jxlpy(jxl画像対応用)がこのMacのPython 3.13環境ではインストールに失敗する。
コード中でインポートされておらず、対応拡張子(.jpg/.jpeg/.png/.webp/.bmp)にも .jxl は無いので、未使用の依存として除外してインストールした。

実行すると今度は T5Tokenizer requires the SentencePiece library で止まった。
requirements.txt には入っていないが、sentencepiece / tiktoken / protobuf が実行時に必要だった。

torchのバージョンも合わせる必要があった。過去のRunPod検証(Anima-Baseへの切り替え記事)ではtorch 2.5.1を使っていたが、Mac arm64のPython 3.13向けには2.5.1が提供されておらず、最も近い2.6.0を入れた。

もう一つ、スクリプト自体がMPSを想定していなかった。
anima_train.py のデバイス判定は次の1行だけで、MPSの分岐が存在しない。

device = "cuda" if torch.cuda.is_available() else "cpu"

CPUのままだと2Bモデルの検証が現実的な時間で終わらないので、MPSを選ぶ分岐を追加した。

if torch.cuda.is_available():
    device = "cuda"
elif torch.backends.mps.is_available():
    device = "mps"
else:
    device = "cpu"

torch 2.6.0でクラッシュした

ここまで直して実行すると、Transformer(685/688キー一致)、VAE(194/194一致)、テキストエンコーダの読み込み、LoRAの316層への注入、データセット読み込みとVAEのlatentキャッシュまでは通った。
最初のフォワードパスで、MPSの matmul カーネルが incompatible dimensions / invalid shape というLLVM ERRORで異常終了した(SIGABRT、exit 134)。落ちた場所のテンソル形状は (1,16,64,128)(1,8,128,64)

Pythonの例外ではなく低レベルのMetal側クラッシュなので、try/except では拾えなかった。

原因はPyTorch本体のMPS/SDPAバグだった

検索すると、scaled_dot_product_attention のMPS実装で同じ症状を報告しているPyTorch本体のissueが複数見つかった。
#147443は、MPS側のSDPAが MetalPerformanceShadersGraph に不適格なサイズのテンソルを渡してクラッシュする件で、PYTORCH_ENABLE_MPS_FALLBACK=1 を試しても回避できないと報告されている。
#149132は、GQA(クエリ側とキー/バリュー側でヘッド数が違う設定)でMPSのSDPAがクラッシュする件で、修正の目標マイルストーンは2.7.0。
#163597は、2.8.0で新しく入った不具合で、非連続(non-contiguous)テンソルに対する高速SDPAカーネルが誤ったストライド前提で壊れる件。2.7.1では発生せず、修正目標は2.9.0とされている。

non-contiguousテンソルまわりのMPSの弱さは、ComfyUIのUpscaleがMac MPSで壊れた件でも一度経験していて、今回のクラッシュも同じ系統の問題だ。
MPSのSDPA実装には、2.6.0付近では特定のテンソル形状を処理できずクラッシュするバグがあり、2.8.0では別のバグが入った。2.7.1はそのあいだの、どちらにも当たらず動くバージョンになる(少なくとも今回のワークロードではこれで通った)。

torch 2.7.1に上げたら5ステップとも最後まで動いた

torchを2.6.0から2.7.1(torchvisionも0.22.1へ)に上げて、他の設定は変えずに再実行した。

epoch=0 step=1 loss=0.098597 lr=1.00e-04 speed=0.11 it/s
epoch=0 step=2 loss=0.062920 lr=1.00e-04 speed=0.33 it/s
epoch=0 step=3 loss=0.171651 lr=1.00e-04 speed=0.35 it/s
epoch=0 step=4 loss=0.073384 lr=1.00e-04 speed=0.39 it/s
epoch=0 step=5 loss=0.017360 lr=1.00e-04 speed=0.38 it/s

5ステップとも例外なく最後まで進み、LoRAも保存された。lora_up.weight はゼロ初期化のはずが、保存後は絶対値の合計が0を大きく上回っていて、重みが実際に変わったことも確認できた。

AnimaLoraToolkit内蔵のサンプリング機能で、学習前後の画像も生成した。

学習前(LoRA重みゼロ初期化、Anima-Base単体相当)は黒髪の女の子が破綻なく出た。
ベースモデルの素の推論なので出て当たり前ではあるが、過去のSDXL失敗ではVAEのfp16オーバーフローによるサンプル破綻が原因候補に挙がっていて、その種の環境起因の破綻なら学習前のサンプルにも出るはず。少なくともMPS上の生成経路が壊れていないことは分かる(過去記事のサンプルはエポック1以降だけで、学習前の時点で破綻していたかは記録がない)。
5ステップ後は別の髪色・獣耳の女の子に変化した。かなちゃん(学習素材の茶髪サイドポニー)の特徴はまだ出ていないが、前回のようにキャラが崩れまくっている事象は起きてないので、普通に回せそうだと確認。

5ステップ版 学習前

5ステップ版 5ステップ後

過去のSDXL/kohya-ss失敗は、パラメータを変えてもエポック1から一貫して崩れた手や「ERROR」の文字が出続けるという、明確な破綻の型だった。
今回のbaseline・5ステップ後にはその手の破綻が出ていない。

300ステップでの安定性検証

5ステップが動いただけでは、最後まで回るかどうかは分からない。過去のSDXL失敗には、エポック1では動いてもエポック2に向かう途中で止まった例もあった。ステップ数を伸ばして、クラッシュや出力の崩壊が途中で出ないかを確認する。

最初は解像度512のまま回そうとしたが、学習素材のかな画像は1024×1024で保存されていて、RunPodでの過去の学習(3キャラ合体LoRA等)も解像度1024で行っている。512のままだと素材をダウンスケールすることになるし、過去の実測(1ステップ2.3秒)とも条件が揃わないので、1024にして回し直した。

かな単体79枚全部を使い、300ステップ(79枚・batch1なので約3.8エポック相当)回す。50ステップごとにサンプル画像を生成して、崩壊が始まる兆候がないか確かめる。

品質は今回あまり気にしていない。過去のキャラLoRA検証では、キャラの特徴が出始めるのはep25以降、それなりに判断できるようになるのがep60〜100あたりというのが経験則で、300ステップ(約3.8エポック)はそもそも通常なら見もしないレベル。知りたいのは「まともなかなちゃんが出るか」ではなく、ステップ数を伸ばしてもクラッシュや崩壊(崩れた手・ERROR文字等)が出ないかで、品質判定より一段階手前の確認になる。

step0〜100の経過

学習前(step0、baseline)は破綻のない黒髪の女の子が出た。

300ステップ版(lr1e-4) step0 baseline

step50では、解剖学的な破綻はないものの、プロンプトに safe タグを入れているにもかかわらず性的な表現が出た。学習データ(かなの表情ポートレート、露出タグなし)には性的な内容は含まれていないので、Anima-Base自体が持つ地の偏り(肌面積の少ない服だと体型依存で盛られやすいという傾向は既存の検証記事でも報告済み)が、LoRAの摂動で表に出た可能性がある(性的な際どさがあるため画像はぼかして掲載)。

300ステップ版(lr1e-4) step50(ぼかし加工済み)

step100では一転して、茶髪のサイドポニー+紫のシュシュ+学生服(白ブラウス+赤リボン+紺スカート)と、学習素材の「かなちゃん」の特徴がはっきり出た。79枚・約1.3エポックの時点でここまで出るのは、経験則(ep25以降でキャラの特徴が出始める)より早い。step50の性的表現は一時的なブレだったとみられる。序盤のサンプルは1枚ごとの振れ幅が大きく、傾向として判断するには早すぎる。

300ステップ版(lr1e-4) step100

いずれの段階でも、崩れた手や「ERROR」の文字のような明確な破綻は出ていない。

step150では、髪色とあほ毛は参照(かなの学習素材)とよく一致した。シュシュはティール(緑寄りの青緑)で、参照の青に近いが完全一致ではない。一方でツインテール(参照は片側だけのサイドポニー)や、翼・凝った衣装のような無関係な要素はまだ残っている。

300ステップ版(lr1e-4) step150

学習率1e-4は本番設定の5倍

79枚・約1.9エポック(150ステップ)の時点でここまで特徴が出るのは、過去のマルチキャラLoRA検証の経験則(ep25以降でキャラの特徴が出始める)よりだいぶ早い。設定を見直すと、learning_rate: 1.0e-4 をそのまま使っていた。

今までは単体(keichan solo)・2人(keikana v2)・3人(trio)・4人(4char)のどれも learning_rate: 2.0e-5 の値を使用してきた。trio記事の設定表には「Anima公式かつ実証の安定値。高いと不安定」と注記したにも関わらず、今回はその5倍のレートで回していた。

AnimaLoraToolkit自体の docs/training-tips.md には、データセット規模別の学習率目安が載っている。

小データセット(<100枚)5e-5〜1e-4 / 中規模(100-500枚)1e-4〜2e-4 / 大規模(500枚+)1e-4〜3e-4

79枚は「小データセット」区分で、目安は5e-5〜1e-4。今回使った1e-4はこの範囲の上限で、toolkit側の一般的な目安としては外れていない。ただしこれはデータセット枚数だけを見た汎用的な目安で、Anima-Baseというアーキテクチャで実際に検証されてきた2e-5とは、数字の出てきた理由が違う。toolkitの既定 lora_type がLoKr(表現力は高いが調整が要る、とtoolkit自身のドキュメントに書いてある)である点からも、既定の学習率はLoKr寄りに決められている可能性がある。

高いLRで1ステップの更新量が大きい分、特徴の出現が早く見えていたと考えると説明がつく。ここまでのstep50〜150の速さは、MacというよりLRの高さが効いている可能性がある。300ステップを最後まで走らせてから、学習率だけを2e-5に直して同条件で再実行し、収束の速さと安定性を比較してみる。

step200・step250では、髪色・あほ毛・サイドポニー・青いシュシュが安定して全部出た。step150で出ていたツインテールの乱れは解消し、翼や凝った衣装のような無関係な要素も出なくなった(目の色だけ参照の茶色とズレて緑〜青寄りになっている)。破綻は最後まで一度も出ていない。

300ステップ版(lr1e-4) step200

300ステップ版(lr1e-4) step250

収束が速すぎる原因を特定できなかった

79枚・300ステップ(約3.8エポック)でここまで特徴が出るのは、当初想定していたマルチキャラLoRAの経験則(ep25以降でキャラの特徴が出始める)よりだいぶ早い。ただし比較対象が違うのではと思い直し、同じ「keichan v2」の記事内で並行して行われていた、kanachan自身をAnima-Baseで焼き直す実験を確認した。こちらは70枚・repeats4・lr2e-5(1エポック=280ステップ)で回していて、Codexの評価は「茶髪・アホ毛・サイドポニー・青シュシュが全epochで出てキャラは安定し、最良はep150」。検証したepochの一番手前がep30なので、安定が確認できている最小値はep30、つまり8,400ステップになる。

今回のMacテストはlr1e-4(5倍)で、特徴が出揃ったstep200を基準に5倍を単純に割り引いても「1,000ステップ相当」にしかならず、8,400ステップとは8倍以上の開きが残る。他に考えられる要因を検討したが、いずれも決め手にならなかった。

検討した要因判定
学習データの均質性「クローズアップ主体で単純だから速い」という説を立てたが、実際に79枚のキャプションを数えたら full body/standing 38枚・close-up portrait 24枚・横顔や後ろ姿やアクションポーズなど17枚と、構図はかなりバラけていた。この説は撤回した
浮動小数点や実装の違いmixed_precision: "fp32"(一番精度が高い設定)で、Muon差し替え用のMuonSwapAdamWは中身が空のtorch.optim.AdamWのサブクラスなので数値的な違いはない。どちらも原因ではない
MPS対CUDAの計算方式の違い同じ数式・同じfp32でも、バックエンド間で数値の完全一致までは保証されない。MPSとCUDAで1ステップの更新量がどの程度違うかは今回比較しておらず、除外まではできない。処理が速いか遅いかとは別の話

一番ありそうなのは判定基準の甘さで、過去の本番検証は複数epoch×複数プロンプト形式のグリッドをCodex(LLM)に採点させて判定している。今回はseed42固定・チェックポイントごとに1枚だけの目視で判断している。同じkeichan v2の記事には「単一seedのアーティファクト(手の破綻・変ポーズ)では、過学習と生成ガチャを切り分けられない」という教訓も書かれていて、今回の見た目の一致も、複数シードで出力し直せば崩れるガチャの可能性がある。

原因を一つに絞り込めないので、lrを2e-5に戻した比較と複数シードでの確認を次に行うことにした。

1ステップの所要時間が区間ごとに上下した

サンプリングのタイムスタンプから逆算すると、学習速度が一定でないことが分かった。

区間所要時間1ステップ平均
step 1〜5013分58秒約16.8秒
step 51〜10020分00秒約24秒
step 101〜15016分35秒約19.9秒
step 151〜20016分21秒約19.6秒
step 201〜25020分23秒約24.5秒

単調に悪化してはいない。24秒→19.9秒→19.6秒→24.5秒と、遅い区間と速い区間を行き来している。サーマルなら基本的に単調悪化か高止まりのはずで、この上下動はサンプリング(model.eval()で25ステップ推論してからmodel.train()に戻す)に伴うMPS側のキャッシュ・メモリ再構成コストの方が辻褄が合う気がするが、断定はできない。CPU70度台・GPU80度台・FAN全開という実測はあるが、この程度でサーマルスロットリングが起きるほど極端な数字ではなさそうだった。

平均すると1ステップおよそ21秒。この値で計算すると、3キャラ合体LoRAと同じ22,050ステップをこのMacで回した場合、22050×21秒 ≈ 129時間 ≈ 約5.3日。ソロキャラの本番相当(前述のkanachan Anima-Base焼き直しのep150 = 42,000ステップ)で計算すると 42000×21秒 ≈ 245時間 ≈ 約10.2日。RunPod(RTX5090)では3キャラ規模が約14時間で終わっているので、単純計算で約9倍の開きになる。ただしこれはresolution:1024mixed_precision:"fp32"・rank4というこの検証固有の設定での数字で、bf16が使えれば縮む可能性はある(過去のSDXL失敗がbf16/fp16まわりだったので、今回は試していない)。

300ステップ、lr=1e-4は最後まで動いた

exit code 0で正常終了し、300ステップの間クラッシュは一度も起きなかった。lossは300ステップ分の記録で、最小0.0068・最大0.109、最初の10ステップ平均0.033に対して最後の10ステップ平均0.039と、下がりきらずに上下し続けた。高LRで1ステップの更新量が大きい状態と整合する。

最終サンプル(step300)は、破綻のない全身立ち絵になった。茶髪・あほ毛・サイドポニー・青シュシュに加えて、学生服(ベージュのカーディガン、白ブラウス、水色のリボン、チェックのプリーツスカート、紺のニーソックス、ローファー、通学バッグ)まで一貫して出た。目の色だけ緑がかっていて、参照(茶色)とはまだズレている。手足の破綻や崩れはない。

300ステップ版(lr1e-4) 最終サンプル(step300)

lr=2e-5での再実行

300ステップが最後まで動いたことを確認したので、learning_rate だけを過去記事すべてで使ってきた2.0e-5に直し(Anima公式の推奨はrank32構成での値なので、今回のrank4での適正値とまでは言えないが、比較用にはこれで揃う)、他の設定(rank4、resolution1024、fp32、かな79枚)は一切変えずに同条件で再実行を始めた。lr1e-4での速い収束がLRの高さで説明がつく現象なのか、それとも判定の甘さなど別の要因が主なのかを切り分けるのと、本番設定に近いLRでMacの学習がどこまで進むかを確かめるためだ。

step0(baseline)はlr1e-4版と完全に同じ数値(mean=0.0126, std=0.6393)で、これは学習前なのでLRに関係なく当然の結果。再現性の確認にもなった。

step50では、水着+猫耳+サングラスという、学習データにない要素が出た。lr1e-4版のstep50でも性的な内容や翼のような無関係要素が出ていたのと同じ傾向で、LRを下げても序盤のブレそのものは消えていない。序盤のブレ自体はLRの高さとは別の話で、まだトリガーワードが固まってないので適当に出てきているだけと思われる。

lr=2e-5 step50、水着+猫耳+サングラス

step100は、金髪+猫耳+はだけたシャツ(黒レース下着)+ピンクカーディガンという、かなちゃんの特徴(茶髪・サイドポニー・青シュシュ)が一切出ていない状態だった。lr1e-4版の同じstep100はすでに茶髪+サイドポニー+紫シュシュ+学生服まで出ていたのと比べると差は明白。本番と同じLR(2e-5)では100ステップ時点でまだほとんど収束しておらず、lr1e-4版の速さはLRの高さがかなりの部分を占めていたと考えられる。

lr=2e-5 step100、金髪+猫耳+はだけたシャツ

step150では髪色が茶に戻ったが、狐耳のファンタジー戦士風衣装+脚を大きく開いた構図になった。かなちゃんの特徴(サイドポニー・青シュシュ・学生服)はまだ出ていない。構図(股間アングル)が性的な意図を強く持つため、この1枚はぼかして掲載する。

lr=2e-5 step150、狐耳+戦士風衣装(ぼかし加工済み)

step50(猫耳)、step100(猫耳)、step150(狐耳)と、獣耳要素が繰り返し出ている。学習データに獣耳は一切含まれていないので、これはAnima-Base側の癖と見られる。サンプリングはsample_seed: 42で全チェックポイント固定しているため、同じシードでは毎回ベースモデルの獣耳方向が出やすいのかもしれない。LoRAの信号は、まだそれを上書きできるほど強くない。別のシードを振るとまた違った出力になっていたと思われる。

もう一つ、そもそも50ステップ刻み(79枚・repeats1だと約0.6エポック相当)という粒度は、普段の検証では使っていない細かさだ。過去の本番検証はエポック単位でサンプルを見ていて、1エポックあたりのステップ数は70枚×repeats4で280、294枚×repeats2で588と、今回よりずっと大きい。チェックポイント間で衣装やキャラがガラッと変わるようなブレは、普段はこの粒度で覗かないから気づかないだけで、どんな学習でも起きている通常のノイズという可能性がある。Mac固有でもLR固有でもなく、単に普段より細かく観測したから見えている、というのが一番シンプルな説明かもしれない。

step200では、茶髪+片側のサイドポニーと参照にかなり近づいた。ただし狐耳は継続し、目は緑(参照は茶色)、衣装は巫女風。

lr=2e-5 step200、茶髪+サイドポニー+狐耳+巫女風衣装

safe タグを毎回入れているにもかかわらず、水着・はだけたシャツ・巫女風衣装と、ほぼ全チェックポイントで際どい方向の構図が出ている。学習データにもプロンプトにも性的要素は無いので、lr1e-4版のstep50で書いたAnima-Baseの地の偏りが、獣耳と同じくここでも出ていると見られる。

step250は明確な性的表現になった(狐耳は継続)。

lr=2e-5 step250(ぼかし加工済み)

lr=2e-5は300ステップで特徴が定着しなかった

exit code 0で正常終了し、こちらもクラッシュは一度も起きなかった。lossは最小0.0074・最大0.224、最初の10ステップ平均0.034に対して最後の10ステップ平均0.036と、lr1e-4版と同様に下がりきらず上下を続けた。

最終サンプル(step300)は、金髪ツインテール+海賊風の凝った衣装(三角帽子、肩章付きジャケット、フリルスカート)になった。かなちゃんの特徴(茶髪・サイドポニー・青シュシュ・あほ毛)は一つも出ていない。というかこれホロライブの宝鐘マリンじゃないだろうか。

lr=2e-5 最終サンプル(step300)

同じ300ステップ・同じシード・同じデータセットで、lr1e-4版は破綻のない全身立ち絵まで到達し、lr2e-5版はかなちゃんと無関係なキャラのままだった。差はlearning_rateの1点だけ。lr1e-4版の速い収束は、ほぼLRの高さのせいだったと見ていい。本番と同じLR(2e-5)では、300ステップ(約3.8エポック)で特徴はまだ定着しなかった。先に確認したkanachanのAnima-Base焼き直し実験(70枚・repeats4・lr2e-5で、安定が確認できている最小値がep30=8,400ステップ)とも矛盾しない結果になった。

所要時間はrun1が2時間15分、run2が1時間26分(後者が速いのは深夜に発生した謎の遅延・干渉の影響と見られ、LRとは無関係)。桁としては同じ2時間前後で、かかった時間はさほど変わらないのに、learning_rateという1変数だけで出来上がったキャラの精度がまったく違う、という対比になった。

run1の最終LoRAを複数シードで検証した

これまでのサンプルは全部sample_seed: 42固定で、単一シードのアーティファクトの可能性が最後まで残っていた。run1(lr1e-4)の最終LoRA(muon_swap_stability.safetensors)をresume_loraで読み込み、sample_seed: 7から3つ(7/8/9)の異なるシードで出力してみた。

3枚とも茶髪・あほ毛・サイドポニー・青シュシュが一貫して出た。step300の結果は単一シードのガチャではなく、ちゃんと特徴が焼きついた結果だと確認できた。

run1 最終LoRA seed7

run1 最終LoRA seed8

run1 最終LoRA seed9

ただし体型、特に胸のサイズが3枚とも誇張されていた。学習前のサンプルから一貫して見えていたAnima-Baseの地の偏りがここでも出ている。

サンプリングに使っていたプロンプトが masterpiece, best quality, score_7, safe, 1girl, solo, kanachan という最小限のものだった点も気になったので、普段のかな画像生成で使っている確立済みのプロンプト(side ponytailahoge、サイドポニーの位置指定、服装指定を明示)に差し替え、同じLoRAで同じ3シード(7/8/9)を出力し直した。

run1 最終LoRA、確立済みプロンプト seed7

run1 最終LoRA、確立済みプロンプト seed8

run1 最終LoRA、確立済みプロンプト seed9

3枚とも体型の誇張が解消され、茶髪・あほ毛・サイドポニー・正しい青のシュシュに加えて、赤ネクタイの制服姿(色違い3パターン)で出てきたが、服装としては割とバラバラ。指定のない服を着ているのでこの辺りがまだ焼きつきが甘い。最小限のプロンプトではAnima-Base側の癖が出やすく、要素を具体的に指定するプロンプトほど狙った結果で安定することを、あらためて確認した。

300ステップの学習は2本ともクラッシュなしで完走し、過去のSDXL/kohya-ss失敗で出続けた崩れた手や「ERROR」文字のような品質側の破綻も、今回は最後まで出なかった。