Initial commit: rapid-anima distillation codebase
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- LICENSE +29 -0
- README.md +121 -0
- configs/phase1_anima.toml +75 -0
- configs/phase1_dataset.toml +34 -0
- docs/distillation.md +468 -0
- docs/dmd2.md +115 -0
- docs/dmdx.md +141 -0
- docs/migration_log.md +583 -0
- docs/operations.md +158 -0
- docs/pcm.md +98 -0
- docs/setup.md +61 -0
- docs/workflow.md +210 -0
- modal_app.py +0 -0
- requirements.txt +4 -0
- samples/dmd2_step500.png +3 -0
- samples/dmdx_step500.png +3 -0
- scripts/anima_finetune_dataset_workflow.json +90 -0
- scripts/anima_verify_workflow.json +90 -0
- scripts/anima_workflow.json +82 -0
- scripts/anima_workflow_phase_a.json +82 -0
- scripts/anima_workflow_phase_a_step1000.json +32 -0
- scripts/anima_workflow_phase_b.json +37 -0
- scripts/anima_workflow_turbo.json +90 -0
- scripts/clean_captions.py +152 -0
- scripts/compare_prompts.txt +6 -0
- scripts/distill/__init__.py +0 -0
- scripts/distill/anima_ladd_disc.py +271 -0
- scripts/distill/anima_loader.py +275 -0
- scripts/distill/dataset.py +84 -0
- scripts/distill/dmd2_official_loss.py +244 -0
- scripts/distill/dmd2_trainer.py +430 -0
- scripts/distill/dmdx_loss.py +195 -0
- scripts/distill/hps_reward.py +85 -0
- scripts/distill/pcm_scheduler.py +107 -0
- scripts/distill/precompute_teacher_x0.py +155 -0
- scripts/distill/r3gan_disc.py +200 -0
- scripts/distill/shortcut_module.py +111 -0
- scripts/distill/sid_loss.py +209 -0
- scripts/distill/train_dmd2_official.py +305 -0
- scripts/distill/train_dmdx.py +248 -0
- scripts/distill/train_draftp.py +250 -0
- scripts/distill/train_ladd.py +293 -0
- scripts/distill/train_pcm.py +250 -0
- scripts/distill/train_reflow.py +246 -0
- scripts/distill/train_shortcut.py +226 -0
- scripts/distill/train_sid.py +236 -0
- scripts/distill/train_sota.py +260 -0
- scripts/distill/train_traj.py +363 -0
- scripts/distill/traj_loss.py +207 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
samples/dmd2_step500.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
samples/dmdx_step500.png filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 daraskme
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
NOTE: This license covers the **code** in this repository only. Models
|
| 26 |
+
produced or fine-tuned using this code are governed by the upstream model
|
| 27 |
+
licenses (CircleStone Labs Non-Commercial License + NVIDIA Open Model License
|
| 28 |
+
Derivative Model terms for Anima). Such derivative models may only be used
|
| 29 |
+
for non-commercial purposes.
|
README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# rapid_anima — Anima 生成速度向上 on Modal
|
| 2 |
+
|
| 3 |
+
CircleStone Labs の [Anima](https://huggingface.co/circlestone-labs/Anima)
|
| 4 |
+
(2B パラメータの DiT)の **生成速度を改善** するための一式。
|
| 5 |
+
旧名 `darask_anima`、2026-05 にスコープを「fine-tune + 蒸留」から「速度向上特化」に転換。
|
| 6 |
+
|
| 7 |
+
> ⚠️ **ライセンス**: Anima は CircleStone Labs Non-Commercial License +
|
| 8 |
+
> NVIDIA Open Model License (Derivative Model 条項) で **非商用のみ**。
|
| 9 |
+
> 派生モデルも非商用縛り。詳細は [末尾のライセンス節](#ライセンス) 参照。
|
| 10 |
+
|
| 11 |
+
## 主な高速化レバー
|
| 12 |
+
|
| 13 |
+
- **B200 sageattention 実 engage** — sm_100 patched wheel で silent fallback 回避
|
| 14 |
+
([darask0/modal_B200_sageattetion_comfyUI](https://huggingface.co/darask0/modal_B200_sageattetion_comfyUI))
|
| 15 |
+
- **batch=8 並列生成** — ComfyUI EmptyLatentImage 経由で 1 submit に 8 枚
|
| 16 |
+
- **蒸留 LoRA** — Civitai 公式 Anima Turbo merge / 自前蒸留 (PCM, Z-Image traj, LADD, Reflow)
|
| 17 |
+
- **ComfyUI 推論最適化** — CapitanFlowMatch scheduler, sageattention, DPM-Solver++ 等
|
| 18 |
+
|
| 19 |
+
### 実測 (B200, 1024² / 30 step)
|
| 20 |
+
|
| 21 |
+
| 構成 | per-image | 5000 枚 cost |
|
| 22 |
+
|---|---|---|
|
| 23 |
+
| 旧 batch=1 (no sage) | ~3.3s | ~$44 |
|
| 24 |
+
| **新 batch=8 + sage** | **~3.05s** | **~$26** |
|
| 25 |
+
| 4-step 蒸留 LoRA + sage | ~0.5-1s | (推論時のみ) |
|
| 26 |
+
|
| 27 |
+
## 配布中の LoRA
|
| 28 |
+
|
| 29 |
+
[**huggingface.co/darask0/anima-distill-loras**](https://huggingface.co/darask0/anima-distill-loras)
|
| 30 |
+
に Anima v1.0 base 用の蒸留 LoRA を集約 (PEFT + ComfyUI 両形式)。
|
| 31 |
+
|
| 32 |
+
| サブディレクトリ | 手法 | step / CFG | ステータス | 詳細 |
|
| 33 |
+
|---|---|---|---|---|
|
| 34 |
+
| [`pcm/`](https://huggingface.co/darask0/anima-distill-loras/tree/main/pcm) | Phased Consistency Model | 4-step / CFG=1.0 | ✅ 配布中 | [docs/pcm.md](docs/pcm.md) |
|
| 35 |
+
| `dmd2/` (近日) | DMD2 + TrigFlow | 4-step / CFG=1.0 | 🟢 訓練中 (resume) | [docs/dmd2.md](docs/dmd2.md) |
|
| 36 |
+
| `dmdx/` (近日) | DMDX (ADM) | 4-step / CFG=1.0 | 🟢 訓練中 (新規実装) | [docs/dmdx.md](docs/dmdx.md) |
|
| 37 |
+
|
| 38 |
+
将来追加検討: LADD / Reflow / Z-Image trajectory / SiD2 など。
|
| 39 |
+
|
| 40 |
+
**ComfyUI 使い方の最短手順**:
|
| 41 |
+
|
| 42 |
+
1. `*_comfy.safetensors` を `ComfyUI/models/loras/` に配置
|
| 43 |
+
2. Anima v1.0 base workflow に `LoraLoaderModelOnly` を挿入、`strength_model: 1.0`
|
| 44 |
+
3. KSampler を **steps=4 / cfg=1.0**、推奨 sampler / scheduler を選択
|
| 45 |
+
|
| 46 |
+
詳細な使い方・訓練詳細は各 LoRA の HF model card / docs/{pcm,dmd2,dmdx}.md を参照。
|
| 47 |
+
|
| 48 |
+
### サンプル画像 (4-step / CFG=1.0 / er_sde + simple)
|
| 49 |
+
|
| 50 |
+
DMD2 と DMDX、それぞれ step 500 時点での生成 (warm-start: Civitai Anima Turbo)。
|
| 51 |
+
同 prompt (`2girls, flandre scarlet, remilia scarlet, touhou, ...`)、同 seed=42。
|
| 52 |
+
|
| 53 |
+
| DMD2 step 500 | DMDX step 500 |
|
| 54 |
+
|---|---|
|
| 55 |
+
|  |  |
|
| 56 |
+
|
| 57 |
+
step 500 ではどちらも warm-start (Civitai Turbo) が支配的で、訓練の差異は微小。
|
| 58 |
+
本番完走 (step 5000) 後に再比較予定。
|
| 59 |
+
|
| 60 |
+
## ディレクトリ構成 (簡略)
|
| 61 |
+
|
| 62 |
+
```
|
| 63 |
+
rapid_anima/
|
| 64 |
+
├── modal_app.py # Modal アプリ本体 (全 function 定義)
|
| 65 |
+
├── configs/ # diffusion-pipe 設定 (Phase 1 fine-tune 用)
|
| 66 |
+
├── scripts/
|
| 67 |
+
│ ├── generate_dataset.py # ComfyUI 経由 self-distill 生成
|
| 68 |
+
│ ├── clean_captions.py # quality/meta タグ除去
|
| 69 |
+
│ ├── *_workflow.json # Anima ComfyUI workflow テンプレ
|
| 70 |
+
│ ├── *_prompts.txt # 各種 prompt セット
|
| 71 |
+
│ └── distill/ # 蒸留実装 (PCM / DMD2 / DMDX / LADD / Reflow / Z-Image 等)
|
| 72 |
+
├── docs/ # 詳細ドキュメント (下記参照)
|
| 73 |
+
├── samples/ # README 用検証サンプル画像
|
| 74 |
+
├── requirements.txt
|
| 75 |
+
└── README.md (このファイル)
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
> **重要**: `modal_app.py` は `configs/` と `scripts/` をそのままの構造で
|
| 79 |
+
> image に同梱する。ファイルをルート直下にフラット配置すると `add_local_dir`
|
| 80 |
+
> で失敗するので注意。
|
| 81 |
+
|
| 82 |
+
## ドキュメント
|
| 83 |
+
|
| 84 |
+
**手法別** (本日 v1.0 base での新規試行):
|
| 85 |
+
|
| 86 |
+
| ドキュメント | 内容 |
|
| 87 |
+
|---|---|
|
| 88 |
+
| [docs/pcm.md](docs/pcm.md) | PCM 蒸留 (5000 step 完走、HF 配布済、$22) |
|
| 89 |
+
| [docs/dmd2.md](docs/dmd2.md) | DMD2 + TrigFlow 蒸留 (resume 進行中、~$50) |
|
| 90 |
+
| [docs/dmdx.md](docs/dmdx.md) | DMDX (ADM、新規実装、arxiv 2507.18569v1 移植、進行中、$30) |
|
| 91 |
+
|
| 92 |
+
**インフラ / セットアップ**:
|
| 93 |
+
|
| 94 |
+
| ドキュメント | 内容 |
|
| 95 |
+
|---|---|
|
| 96 |
+
| [docs/setup.md](docs/setup.md) | Modal セットアップ、HF シークレット、データセット用意 |
|
| 97 |
+
| [docs/workflow.md](docs/workflow.md) | Step 0-5 (モデル DL / dataset / Phase 1 / 検証 / 蒸留)、GPU 選び、コスト概算 |
|
| 98 |
+
| [docs/distillation.md](docs/distillation.md) | Anima 蒸留 — R3GAN 5 連続失敗の deep dive、設計理由、教訓 |
|
| 99 |
+
| [docs/migration_log.md](docs/migration_log.md) | 2026-05 既存実装移植 (Z-Image / DMD2 / LADD / Reflow / PCM)、比較結果、reward hacking 実証 |
|
| 100 |
+
| [docs/operations.md](docs/operations.md) | Modal CLI / diffusion-pipe 罠、実測コスト、トラブルシューティング |
|
| 101 |
+
|
| 102 |
+
## 参考資料
|
| 103 |
+
|
| 104 |
+
- 公式モデルカード: https://huggingface.co/circlestone-labs/Anima
|
| 105 |
+
- diffusion-pipe: https://github.com/tdrussell/diffusion-pipe
|
| 106 |
+
- DMD2 論文 (Z-Image): https://arxiv.org/abs/2511.22677
|
| 107 |
+
- LCM 論文: https://arxiv.org/abs/2310.04378
|
| 108 |
+
- Hyper-SD: https://github.com/bytedance/hyper-sd
|
| 109 |
+
- RDBT-Anima (Anima 蒸留先行例): https://civitai.com/models/2364703
|
| 110 |
+
- PCM 元論文: [Wang et al. NeurIPS 2024](https://arxiv.org/abs/2405.18407) / [G-U-N/Phased-Consistency-Model](https://github.com/G-U-N/Phased-Consistency-Model)
|
| 111 |
+
|
| 112 |
+
## ライセンス
|
| 113 |
+
|
| 114 |
+
このリポジトリ自体のコードは MIT で公開。生成される派生モデル(蒸留 LoRA や
|
| 115 |
+
fine-tune 結果)は **Anima のライセンス** に従う必要があり、**非商用のみ**:
|
| 116 |
+
|
| 117 |
+
- CircleStone Labs Non-Commercial License
|
| 118 |
+
- NVIDIA Open Model License(Derivative Model 条項)
|
| 119 |
+
|
| 120 |
+
詳細・商用利用問い合わせは Anima 公式ページを参照:
|
| 121 |
+
https://huggingface.co/circlestone-labs/Anima
|
configs/phase1_anima.toml
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# Anima Phase 1: quality タグ依存性除去 (LoRA)
|
| 3 |
+
# 目的: "masterpiece, best quality, score_9, ..." を書かなくても base が出していた
|
| 4 |
+
# 品質の絵が出るようにする。**審美の方向性は変えない**(artist タグ等は維持)。
|
| 5 |
+
# 性質: 概念追加でなく "prior shift" → rank 小・lr 低・epochs 少で十分。
|
| 6 |
+
# 過剰に回すと base の汎用性を損なう (catastrophic forgetting)。
|
| 7 |
+
# 使用: deepspeed --num_gpus=1 train.py --deepspeed --config phase1_anima.toml
|
| 8 |
+
# =============================================================================
|
| 9 |
+
|
| 10 |
+
output_dir = '/output/phase1'
|
| 11 |
+
|
| 12 |
+
# データセット設定 (別ファイル)
|
| 13 |
+
dataset = '/workspace/configs/phase1_dataset.toml'
|
| 14 |
+
|
| 15 |
+
# 学習設定 ---------------------------------------------------------------------
|
| 16 |
+
# epochs: prior shift だけなら 2 で十分。3 にすると過学習リスク微増。
|
| 17 |
+
# 1 epoch ≈ 3-4h (A100-80GB, 5k枚) → 2 epoch ≈ 6-8h ≈ $15-20
|
| 18 |
+
# 10k 枚使う場合: 1 epoch ≈ 6-7h → 2 epoch ≈ 12-14h ≈ $30-35
|
| 19 |
+
epochs = 2
|
| 20 |
+
micro_batch_size_per_gpu = 1
|
| 21 |
+
pipeline_stages = 1 # 単 GPU は 1
|
| 22 |
+
gradient_accumulation_steps = 4 # 実効 batch = 4 (5k枚以下なら 2 でも可)
|
| 23 |
+
gradient_clipping = 1.0
|
| 24 |
+
warmup_steps = 100
|
| 25 |
+
|
| 26 |
+
# CPU offload (blocks_to_swap > 0 で VRAM 節約だが速度低下。A100-80GB なら 0 でOK)
|
| 27 |
+
blocks_to_swap = 0
|
| 28 |
+
|
| 29 |
+
# 評価/保存
|
| 30 |
+
eval_every_n_epochs = 1
|
| 31 |
+
save_every_n_epochs = 1 # 古い ckpt は cleanup_checkpoints で間引く
|
| 32 |
+
checkpoint_every_n_minutes = 60 # 安全保険 (途中落ち対策)
|
| 33 |
+
activation_checkpointing = true
|
| 34 |
+
save_dtype = 'bfloat16'
|
| 35 |
+
|
| 36 |
+
# モデル ----------------------------------------------------------------------
|
| 37 |
+
[model]
|
| 38 |
+
type = 'anima'
|
| 39 |
+
# Anima preview3-base (公式 ComfyUI 形式)
|
| 40 |
+
transformer_path = '/models/checkpoints/anima-preview3-base.safetensors'
|
| 41 |
+
vae_path = '/models/checkpoints/qwen_image_vae.safetensors'
|
| 42 |
+
llm_path = '/models/checkpoints/qwen_3_06b_base.safetensors'
|
| 43 |
+
dtype = 'bfloat16'
|
| 44 |
+
transformer_dtype = 'bfloat16'
|
| 45 |
+
|
| 46 |
+
# 公式必須ルール: LLM adapter は学習しない
|
| 47 |
+
# (テキスト埋め込み変換器で、訓練すると壊れやすい)
|
| 48 |
+
llm_adapter_lr = 0
|
| 49 |
+
|
| 50 |
+
# Anima は flow matching ベース。timestep サンプリングは logit_normal が安定。
|
| 51 |
+
timestep_sample_method = 'logit_normal'
|
| 52 |
+
|
| 53 |
+
# LoRA --------------------------------------------------------------------
|
| 54 |
+
[adapter]
|
| 55 |
+
type = 'lora'
|
| 56 |
+
rank = 64 # 審美 FT は概念追加でないので 32〜64 で十分
|
| 57 |
+
alpha = 64 # = rank だと scale 1.0
|
| 58 |
+
dropout = 0.0
|
| 59 |
+
dtype = 'bfloat16'
|
| 60 |
+
# 対象モジュール: DiT の attention/MLP のみ (LLM adapter は除外)
|
| 61 |
+
# diffusion-pipe は anima 用にデフォルトで適切な層を選んでくれる
|
| 62 |
+
|
| 63 |
+
# Optimizer ---------------------------------------------------------------
|
| 64 |
+
[optimizer]
|
| 65 |
+
type = 'AdamW8bitKahan' # bf16 で安定、VRAM 節約
|
| 66 |
+
lr = 2e-5 # 公式推奨。広げるなら 1e-5〜5e-5
|
| 67 |
+
betas = [0.9, 0.99]
|
| 68 |
+
weight_decay = 0.01
|
| 69 |
+
eps = 1e-8
|
| 70 |
+
stabilize = false
|
| 71 |
+
|
| 72 |
+
# Monitoring --------------------------------------------------------------
|
| 73 |
+
[monitoring]
|
| 74 |
+
log_every_n_steps = 10
|
| 75 |
+
enable_wandb = false # wandb 使うなら true + secret 追加
|
configs/phase1_dataset.toml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# Phase 1 データセット設定
|
| 3 |
+
# 期待構造: /dataset/cleaned/<image>.{png,jpg,webp} と同名の <image>.txt
|
| 4 |
+
# (clean_captions.py 実行後の出力先)
|
| 5 |
+
# =============================================================================
|
| 6 |
+
|
| 7 |
+
resolutions = [1024] # Anima 推奨 1MP
|
| 8 |
+
enable_ar_bucket = true # アスペクト比バケット
|
| 9 |
+
min_ar = 0.5 # 縦長 1:2 まで
|
| 10 |
+
max_ar = 2.0 # 横長 2:1 まで
|
| 11 |
+
num_ar_buckets = 9
|
| 12 |
+
frame_buckets = [1] # 画像のみ
|
| 13 |
+
|
| 14 |
+
# キャプション関係 ----------------------------------------------------------
|
| 15 |
+
# danbooru タグ風キャプションを ", " 区切りでシャッフル
|
| 16 |
+
shuffle_tags = true
|
| 17 |
+
# 学習を頑健にするため一部タグをランダムに drop
|
| 18 |
+
# (品質タグはすでに clean_captions.py で除去済みだが、artist 等の頑健化に有効)
|
| 19 |
+
caption_dropout_rate = 0.1
|
| 20 |
+
# プロンプトをまるごと空にする確率 (CFG 学習の暗黙化、5〜10% が定番)
|
| 21 |
+
text_dropout_rate = 0.05
|
| 22 |
+
|
| 23 |
+
# テキストエンコーダ出力の事前キャッシュ。
|
| 24 |
+
# 効果: 各 step の Qwen3 forward (~0.2s) を省略 → 全体 15-25% 高速化 ≈ $7-13/run
|
| 25 |
+
# 制約: 上の shuffle_tags / *_dropout_rate と相反 (キャッシュは固定文字列で計算)
|
| 26 |
+
# 速度優先する場合: 下記 ↑ の shuffle_tags=false, caption_dropout_rate=0,
|
| 27 |
+
# text_dropout_rate=0 にしてこれを true にする。
|
| 28 |
+
cache_text_embeddings = false
|
| 29 |
+
|
| 30 |
+
# 実データセット --------------------------------------------------------------
|
| 31 |
+
[[directory]]
|
| 32 |
+
path = '/dataset/cleaned'
|
| 33 |
+
# クラス重み付け (複数 dataset を混ぜる時に使う。今は 1.0)
|
| 34 |
+
num_repeats = 1
|
docs/distillation.md
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Anima 蒸留 — 実走で分かった知見
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md)
|
| 4 |
+
|
| 5 |
+
> **TL;DR**: 数万円予算で Anima を自前 SOTA 蒸留(DMD2+TSCD+R3GAN)するのは
|
| 6 |
+
> 現実的に困難。**5 回失敗で累計 $165**。実用解は **`merge_turbo_lora`**
|
| 7 |
+
> (公式 Anima Turbo LoRA との合成、$0.5、即動く)。自前で挑む人向けに、
|
| 8 |
+
> 何が起きるか・なぜ難しいかを以下に集約。
|
| 9 |
+
>
|
| 10 |
+
> 2026-05 以降の改善試行 (PCM / LADD / Reflow など既存実装の移植) は
|
| 11 |
+
> [migration_log.md](migration_log.md) を参照。
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## 1. 概要(結論先出し)
|
| 16 |
+
|
| 17 |
+
このリポジトリでは Anima(CircleStone Labs, Cosmos-Predict2 派生 2B DiT)を
|
| 18 |
+
**Decoupled DMD2 + TSCD + R3GAN** で **step 削減蒸留** することを目指した。
|
| 19 |
+
B200 GPU(Modal)で 5000 枚 self-distillation データセットを使い、**計 5 回の失敗**:
|
| 20 |
+
|
| 21 |
+
| 試行 | 構成 | 結果 | コスト |
|
| 22 |
+
|---|---|---|---|
|
| 23 |
+
| Phase A | full DiT, R3GAN gamma=50 | 完全ノイズ(D 爆発) | $70 |
|
| 24 |
+
| Phase B v1 | Q,V LoRA, R3GAN OFF | 緑色化(AdaLN 不触) | $10 |
|
| 25 |
+
| Phase B v2 | wide LoRA, R3GAN OFF | mean collapse | $10 |
|
| 26 |
+
| Phase B v3a | wide LoRA, gamma=0.1 | step 30 で D 爆発 abort | $5 |
|
| 27 |
+
| **Phase B v3b** | wide LoRA, gamma=1.0 | **mean collapse + 後で D 爆発** | $20 |
|
| 28 |
+
|
| 29 |
+
**最終理解**: R3GAN は「D 爆発」と「無信号 → mean collapse」の間に **安定 zone が
|
| 30 |
+
極めて狭く**、Anima latent(16ch × 128²)で持続的な訓練は現状の構成では困難。
|
| 31 |
+
DMDR / Z-Image の公式実装が **DINOv2 reward に依存** していることからも、
|
| 32 |
+
adv loss 単独の R3GAN だけでは Anima 系の蒸留は厳しい可能性が高い。
|
| 33 |
+
|
| 34 |
+
### 1.1 結局どうすればいいか
|
| 35 |
+
|
| 36 |
+
| 用途 | 推奨パス | コスト | 結果 |
|
| 37 |
+
|---|---|---|---|
|
| 38 |
+
| **すぐ 8-12 step 推論したい** | `merge_turbo_lora`(公式 Civitai Turbo LoRA を Anima base に重ねる)| **$0.5** | ✅ 即動く、CFG=1 / 8-12 step |
|
| 39 |
+
| **どうしても自前で蒸留したい** | 本ドキュメントの試行錯誤を踏まえ、**DINOv2 reward 切替** 等を追加実装 | +$50-100 + 開発時間数日 | △ 未検証 |
|
| 40 |
+
| **学習目的・記録目的** | 本ドキュメントを読む | $0 | 知見だけ |
|
| 41 |
+
|
| 42 |
+
> **強い推奨**: 商用要件がなければ `merge_turbo_lora` で十分。CircleStone Labs
|
| 43 |
+
> 自身が遥かに多い計算資源と試行錯誤の上で作った Turbo LoRA が既に Civitai
|
| 44 |
+
> にある(https://civitai.com/models/2560840)、これに勝つのは数万円では困難。
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## 2. 結局成功した実用パス: `merge_turbo_lora`
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
# 1. Anima Turbo LoRA を Civitai からダウンロード
|
| 52 |
+
modal run modal_app.py::download_civitai_lora # version_id=2877687 がデフォルト
|
| 53 |
+
|
| 54 |
+
# 2. (任意) 自前 Phase 1 LoRA とマージ(なくても可)
|
| 55 |
+
modal run modal_app.py::merge_turbo_lora
|
| 56 |
+
|
| 57 |
+
# 3. ComfyUI で base + LoRA(strength=1.0)を組み、8 step CFG=1 で生成
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
実装: `modal_app.py::download_civitai_lora`, `merge_turbo_lora`, `stage_lora_to_models`。
|
| 61 |
+
ComfyUI workflow は `scripts/anima_workflow_turbo.json` 参照。
|
| 62 |
+
|
| 63 |
+
実測の Turbo LoRA 出力品質: **base 30 step CFG=4.5 と同等のキャラクター品質を
|
| 64 |
+
8 step CFG=1 で達成**(検証画像は `/dataset/compare/turbo_8step/` に保存)。
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## 3. 自前 SOTA 蒸留(本リポジトリで実装、未完成)
|
| 69 |
+
|
| 70 |
+
`scripts/distill/` に Decoupled DMD2 + TSCD + R3GAN を実装。**コード自体は動く** が、
|
| 71 |
+
ハイパーパラメータの調整で 5 回失敗。以下「我々が試した推奨設定」だが、これでも
|
| 72 |
+
2000 step で失敗 — **これから挑む人は次節 §4 §5 を読んでから**。
|
| 73 |
+
|
| 74 |
+
### 3.1 試した(が安定しなかった)設定
|
| 75 |
+
|
| 76 |
+
```text
|
| 77 |
+
=== モデル / データ ===
|
| 78 |
+
base model: Anima preview3-base (Cosmos-Predict2 派生)
|
| 79 |
+
text encoder: Qwen3-0.6B + LLM Adapter (T5 空間ブリッジ)
|
| 80 |
+
VAE: WanVAE (16-channel latent, scale=[mean, 1/std])
|
| 81 |
+
dataset: self-distillation 5000 枚(base が 30 step CFG 4.5 で生成)
|
| 82 |
+
解像度 1024px 周辺の 7 種 aspect ratio をランダム
|
| 83 |
+
|
| 84 |
+
=== 学習 (LoRA-only) ===
|
| 85 |
+
trainable: gen + guidance ともに LoRA、base は frozen
|
| 86 |
+
gen LoRA: **wide** (all-linear except llm_adapter), rank=32
|
| 87 |
+
→ AdaLN modulation を含む全 Linear に attach
|
| 88 |
+
guidance LoRA: Q,V LoRA, rank=32 (DMDR 互換、軽量)
|
| 89 |
+
optimizer: gen / guidance: AdamW lr=2e-5 β=(0.9, 0.999) wd=0.01
|
| 90 |
+
D: Adam lr=2e-4 β=(0, 0.99) (StyleGAN 流)
|
| 91 |
+
grad clip: 1.0
|
| 92 |
+
|
| 93 |
+
=== Loss 構成 ===
|
| 94 |
+
L_dmd: DMDR gradient trick (p_real - p_fake) / norm
|
| 95 |
+
L_consist: TSCD MSE vs EMA generator (decay=0.999)
|
| 96 |
+
L_adv (gen): R3GAN relativistic RpGAN, weight=0.05
|
| 97 |
+
L_disc: R3GAN softplus + γ/2 · (R1 + R2)
|
| 98 |
+
γ = 0.05〜0.5 (grid search 必須)
|
| 99 |
+
|
| 100 |
+
=== 更新比率 / スケジュール ===
|
| 101 |
+
guidance update: 5 回 / outer step
|
| 102 |
+
generator update: 1 回 / outer step
|
| 103 |
+
discriminator: 1 回 / outer step
|
| 104 |
+
CFG Augmentation: real_score を cfg=2.0 で評価 (cold_start 100 step は cfg=0)
|
| 105 |
+
dynamic LoRA r: real-side scale を cosine で 0 に decay (decay_steps=2000)
|
| 106 |
+
|
| 107 |
+
=== Phase 構成 ===
|
| 108 |
+
Phase B: base 直接 + wide LoRA, num_inference_steps=4, 2000 step
|
| 109 |
+
Phase C: Phase B から resume または base 直接, wide LoRA, num_steps=2, 2000 step
|
| 110 |
+
※ Full DiT 訓練 (Phase A) は不要 — LoRA-only で十分
|
| 111 |
+
|
| 112 |
+
=== Sanity / Guard ===
|
| 113 |
+
sanity test: 200 step + 中間生成 1 枚で degeneration 検出
|
| 114 |
+
abort guards: d_r1 > 1000 / d_r2 > 1000 / loss > 1e6
|
| 115 |
+
checkpoint: 500 step 毎 + 都度 volume.commit()
|
| 116 |
+
PYTHONUNBUFFERED=1 + cwd=/workspace/diffusion-pipe
|
| 117 |
+
|
| 118 |
+
=== Modal infra ===
|
| 119 |
+
GPU: B200 (192GB) — LoRA-only で余裕、batch=4
|
| 120 |
+
image: CUDA 12.4 + Python 3.11 + diffusion-pipe + torchvision
|
| 121 |
+
volumes: anima-models / anima-dataset / anima-outputs
|
| 122 |
+
secrets: hf_token, civitai_api_key
|
| 123 |
+
|
| 124 |
+
=== ComfyUI 出力形式 ===
|
| 125 |
+
LoRA key: diffusion_model.<module>.lora_A.weight / .lora_B.weight
|
| 126 |
+
(PEFT の base_model.model.<...>.lora_A.default.weight から変換)
|
| 127 |
+
基底 ckpt: anima ckpt 系は net. prefix 必須 (ComfyUI が arch 検出に使う)
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
実装は `scripts/distill/` + `modal_app.py::train_sota_distill` を参照。
|
| 131 |
+
|
| 132 |
+
> ⚠️ **重要**: 上の設定で 2000 step training を 5 回試したが、いずれも失敗。
|
| 133 |
+
> Anima latent では R3GAN の安定 zone が極めて狭く、長期持続が困難だった。
|
| 134 |
+
> **これから挑む人は §4(全失敗の詳細)+ §5(残された改善方向)を必ず読むこと**。
|
| 135 |
+
|
| 136 |
+
### 3.2.1 なぜ LoRA-only(full DiT 訓練ではなく)
|
| 137 |
+
|
| 138 |
+
| | full DiT(Phase A) | LoRA-only(Phase B/C) |
|
| 139 |
+
|---|---|---|
|
| 140 |
+
| trainable params | 2B | 30-150M |
|
| 141 |
+
| step 時間 (B200) | 14 秒 | **2.8-3.0 秒**(5x 速) |
|
| 142 |
+
| 出力 | 4 GiB ckpt | **30-150 MiB LoRA** |
|
| 143 |
+
| 配布性 | 他 FT と排他 | **他 LoRA とスタック可** |
|
| 144 |
+
| optimizer state | 8 GB | ~200 MB |
|
| 145 |
+
| failure 局所化 | base 全体破壊リスク | LoRA だけ |
|
| 146 |
+
|
| 147 |
+
Anima のように既に高品質な base を蒸留する場合、LoRA-only がコスト・配布性・
|
| 148 |
+
安全性のすべてで優位。Phase A 流の full DiT 訓練は不要だった。
|
| 149 |
+
|
| 150 |
+
### 3.2.2 なぜ「wide LoRA」(Q,V LoRA ではなく)
|
| 151 |
+
|
| 152 |
+
Anima/Cosmos の DiT は **AdaLN modulation で timestep 情報を処理**する架構:
|
| 153 |
+
|
| 154 |
+
```
|
| 155 |
+
block(x, t):
|
| 156 |
+
t_embed = t_embedder(t)
|
| 157 |
+
# AdaLN modulation = block_input をどう変調するかを timestep から決める
|
| 158 |
+
scale, shift, gate = adaln_modulation(t_embed) ← ここが timestep の入り口
|
| 159 |
+
x = LayerNorm(x) * (1 + scale) + shift
|
| 160 |
+
x = x + gate * attention(x, ...)
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
蒸留とは「**timestep の解釈を変える**」こと(少ない step でも denoise しきる)。
|
| 164 |
+
それには adaln_modulation の Linear を学習対象に含める必要がある。Q,V LoRA だと
|
| 165 |
+
attention pattern だけ変えられて timestep 入り口を触れない → 学習が degenerate
|
| 166 |
+
な「色 / スタイル shift」に逃げる(Phase B v1 の緑色化はこれ)。
|
| 167 |
+
|
| 168 |
+
公式 Anima Turbo LoRA も同じ理由で adaln_modulation を target(1016 keys、
|
| 169 |
+
我々の wide LoRA 980 keys とほぼ同等)。
|
| 170 |
+
|
| 171 |
+
### 3.2.3 なぜ R3GAN が必要(adv loss なしではダメ)
|
| 172 |
+
|
| 173 |
+
DMD2 単体は **mean collapse** に陥る(Phase B v2 で実証)。理由:
|
| 174 |
+
- DMD gradient `(real_score - fake_score)` は「real distribution の方向」を示す
|
| 175 |
+
- LoRA capacity が大きいほど、「real の **mean** を出力するのが最も安全」という解に収束しやすい
|
| 176 |
+
- TSCD(EMA self との consistency)は collapse を加速こそすれ防げない
|
| 177 |
+
|
| 178 |
+
R3GAN(または同等の adv / reward 信号)があると:
|
| 179 |
+
- D が「real は sharp、fake は blurry」と学習
|
| 180 |
+
- gen は「blurry な mean を出すと D に見抜かれる」と分かり、sharp な多様性を保つ
|
| 181 |
+
- これが mean collapse の唯一の防御策
|
| 182 |
+
|
| 183 |
+
実際、DMD2 / Hyper-SD / DMDR 系の全公式実装で adv loss(or reward loss)が必須。
|
| 184 |
+
|
| 185 |
+
### 3.2.4 なぜ R3GAN gamma の grid search が必須
|
| 186 |
+
|
| 187 |
+
R3GAN の R1+R2 gradient penalty 重み `gamma` は **入力空間のスケールに比例**:
|
| 188 |
+
|
| 189 |
+
| ドメイン | 入力 channel | 解像度 | gamma |
|
| 190 |
+
|---|---|---|---|
|
| 191 |
+
| CIFAR-10 | 3 | 32x32 | **0.05** |
|
| 192 |
+
| FFHQ-64 | 3 | 64x64 | 2 |
|
| 193 |
+
| FFHQ-256 | 3 | 256x256 | 150 |
|
| 194 |
+
| **Anima latent** | **16** | **128x128** | **~0.05-1**(grid search 必須) |
|
| 195 |
+
|
| 196 |
+
Anima latent は per-sample 262k 要素で、典型 pixel 空間より gradient norm が
|
| 197 |
+
大きい → gamma は数値が小さくないと penalty が爆発。Phase A は gamma=50 で **500x 過大**
|
| 198 |
+
にしたら discriminator が崩壊した(詳細は §4 Phase A 参照)。
|
| 199 |
+
|
| 200 |
+
### 3.2.5 なぜ guard rail(`d_r1/r2 > 1000` で abort)
|
| 201 |
+
|
| 202 |
+
Phase A は **loss は全部健全に推移**(`l_dmd` 0.005, `l_adv_g` 0.69 で一定)しつつ
|
| 203 |
+
裏で discriminator gradient が爆発し、completed ckpt が完全ノイズ出力という
|
| 204 |
+
**ステルス失敗**。後付けの分析で「step 1500 時点 d_r1 ~700 / step 2300 で 500k」と
|
| 205 |
+
わかったため、しきい値 1000 で abort すれば即停止できる。
|
| 206 |
+
|
| 207 |
+
しきい値 1000 の根拠: 正常上限 ~100 の 10x、致命傷 500k の 1/500 で、確実に
|
| 208 |
+
ステルス damage が始まる前に止まる。
|
| 209 |
+
|
| 210 |
+
### 3.2.6 なぜ Decoupled DMD2 + TSCD(LCM / Hyper-SD ではなく)
|
| 211 |
+
|
| 212 |
+
| 手法 | 性質 | 我々の選択理由 |
|
| 213 |
+
|---|---|---|
|
| 214 |
+
| LCM | consistency 単体、シンプル | low-step 品質に限界 |
|
| 215 |
+
| Hyper-SD | TSCD + adv、segment-wise progressive | gamma calibration 必要、参照実装 SDXL only |
|
| 216 |
+
| **DMD2 / DMDR** | **分布マッチング + adv、Anima のような flow matching DiT に最適** | Z-Image(Cosmos 派生)で実証済、適応容易 |
|
| 217 |
+
|
| 218 |
+
DMD2 系は **flow matching ベース(Anima の rectified flow と同じ)** で、math
|
| 219 |
+
変換不要なのが決定打。TSCD は Hyper-SD から借りた多 step 一貫性で、純粋 DMD2
|
| 220 |
+
の単 step 偏りを補正。
|
| 221 |
+
|
| 222 |
+
### 3.2.7 なぜ 5000 枚 self-distillation データセット
|
| 223 |
+
|
| 224 |
+
| データソース | コスト | 利点 | 欠点 |
|
| 225 |
+
|---|---|---|---|
|
| 226 |
+
| Anima 自己生成 | $44 (10 B200 並列 40min) | base の出力分布に最適化 / ライセンス問題なし | base の癖をそのまま継承 |
|
| 227 |
+
| Danbooru / Pixiv | $0 + キュレーション時間 | 多様性 | ライセンス・タグ品質ばらつき |
|
| 228 |
+
| 公開データセット(LAION 等) | $0 + DL コスト | 多様性最大 | アニメ系少ない |
|
| 229 |
+
|
| 230 |
+
蒸留の本質は「**base と同じ出力を低 step で**」なので、self-distillation で
|
| 231 |
+
分布を base に一致させるのが理論的に正しい(distillation の定義)。
|
| 232 |
+
|
| 233 |
+
### 3.2.8 なぜ B200(H100 / A100 ではなく)
|
| 234 |
+
|
| 235 |
+
| GPU | Modal $/h | LoRA-only step 時間 | 192GB VRAM の意味 |
|
| 236 |
+
|---|---|---|---|
|
| 237 |
+
| A100-80GB | $2.50 | 7-9 秒 | OOM 危険、batch=1 |
|
| 238 |
+
| H100-80GB | $3.95 | 4-6 秒 | OOM 危険、batch=1-2 |
|
| 239 |
+
| **B200** | **$6.25** | **2.8-3.0 秒** | **3 model 同時 + batch=4-8 余裕** |
|
| 240 |
+
|
| 241 |
+
B200 の per-hour 単価は高いが、**3 model(real/fake/gen)同時+batch 増**で
|
| 242 |
+
**実質的な per-experiment コストは最安**。R3GAN を入れても OOM 不安なし。
|
| 243 |
+
|
| 244 |
+
---
|
| 245 |
+
|
| 246 |
+
## 4. 実際の失敗(全 5 件、詳細)
|
| 247 |
+
|
| 248 |
+
| # | 試行 | 構成 | 結果 | 累計 |
|
| 249 |
+
|---|---|---|---|---|
|
| 250 |
+
| 1 | **Phase A** | full DiT + Q,V LoRA, R3GAN γ=**50**, 3000 step | 完全ノイズ(D 爆発) | $70 |
|
| 251 |
+
| 2 | **Phase B v1** | Q,V LoRA, R3GAN **OFF**, 2000 step | 緑色化(AdaLN 不触) | $80 |
|
| 252 |
+
| 3 | **Phase B v2** | wide LoRA, R3GAN **OFF**, 2000 step | mean collapse(灰緑平面)| $90 |
|
| 253 |
+
| 4 | **Phase B v3a** | wide LoRA, R3GAN γ=**0.1**, 2000 step | step 30 で d_r1=1648 → abort | $95 |
|
| 254 |
+
| 5 | **Phase B v3b** | wide LoRA, R3GAN γ=**1.0**, 2000 step | 早期 mean collapse → 後半 D 爆発 | $115 |
|
| 255 |
+
|
| 256 |
+
各失敗が **異なる failure mode** で起きた点が重要。「1 つ直しても他で詰まる」
|
| 257 |
+
直交性が、SOTA 蒸留を Anima 系で安定させる難しさの本質。
|
| 258 |
+
|
| 259 |
+
### 4.1 Phase A の詳細 postmortem(5 層因果分析)
|
| 260 |
+
|
| 261 |
+
**ステルス失敗**: 訓練ログ上は全 loss 健全(`l_dmd` 0.003-0.008, `l_adv_g` 0.69 一定)
|
| 262 |
+
だったのに、完走 ckpt が完全ノイズ出力。原因は 5 層の連鎖:
|
| 263 |
+
|
| 264 |
+
**Layer 1 — gamma calibration ミス(根本)**
|
| 265 |
+
- Anima latent(16ch × 128x128)に対し gamma=50 は 50-500 倍過大
|
| 266 |
+
- Agent リサーチが「latent norm 用 grid search 必須」と明示警告していたが、
|
| 267 |
+
手抜きで推測値を採用したのが発端
|
| 268 |
+
|
| 269 |
+
**Layer 2 — R3GAN gradient が gen に流入(見えない damage)**
|
| 270 |
+
```
|
| 271 |
+
l_adv_g = softplus(-(d_fake - d_real)) ≈ log 2 = 0.6914 ← 中立点
|
| 272 |
+
```
|
| 273 |
+
`l_adv_g` が一定で「adv 信号効いてない」と誤読。**しかし gradient norm は別物**:
|
| 274 |
+
```
|
| 275 |
+
∂L_adv_g/∂gen_output = (sigmoid 微分 ~0.5) · ∂(D の全層 Jacobian)
|
| 276 |
+
```
|
| 277 |
+
D 重みが gamma penalty 最小化で異常拡大 → D の Jacobian 爆発 → gen に流れる
|
| 278 |
+
gradient が正常時の **数百〜数千倍**。adv_weight=0.05 で抑えても scale 負け。
|
| 279 |
+
|
| 280 |
+
> 教訓: **adv_loss の絶対値だけ見て安全と判断するな**。gradient norm を別途モニタ。
|
| 281 |
+
|
| 282 |
+
**Layer 3 — D 自体の破滅スパイラル**
|
| 283 |
+
```
|
| 284 |
+
最初: l_disc = 0.69 + 25·0 = 0.69
|
| 285 |
+
膨張後: l_disc = 0.69 + 25·600,000 = 15,000,000
|
| 286 |
+
```
|
| 287 |
+
gamma が大きすぎて R1+R2 penalty 項が adv 項を支配 → D は adv 学習する余地なく、
|
| 288 |
+
ひたすら penalty 最小化に動く → ある瞬間 scale 崩れて d_r1/r2 が指数的に膨張。
|
| 289 |
+
|
| 290 |
+
**Layer 4 — Sanity test の見落とし**
|
| 291 |
+
- 50 step では destabilization が緩やかすぎて d_r1/r2 が正常範囲
|
| 292 |
+
- 本番 step 500-1000 から急速悪化
|
| 293 |
+
- 後から見れば step 1000 で d_r1/r2 が 100 超え始めていた($50 救えた)
|
| 294 |
+
|
| 295 |
+
> 教訓: **Sanity は 200-500 step に延長**。緩慢な発散も検出可能に。
|
| 296 |
+
|
| 297 |
+
**Layer 5 — 監視/アラート不足**
|
| 298 |
+
- d_r1/r2 はログ出していたが **しきい値アラート未実装**
|
| 299 |
+
- NaN/Inf check はあったが「大きすぎる値」検出なし
|
| 300 |
+
- gen �� weight L2 norm を時系列で見れば damage 早期検出可能だった
|
| 301 |
+
- 中間 ckpt 毎の自動生成テストも未実装
|
| 302 |
+
|
| 303 |
+
修正後の guard rail(実装済):
|
| 304 |
+
```python
|
| 305 |
+
# scripts/distill/dmd2_trainer.py
|
| 306 |
+
if d_r1 > 1000.0 or d_r2 > 1000.0:
|
| 307 |
+
raise RuntimeError(f"R3GAN penalty exploded (r1={d_r1:.1f}, r2={d_r2:.1f}). "
|
| 308 |
+
f"Reduce r3gan_gamma. Aborting.")
|
| 309 |
+
```
|
| 310 |
+
|
| 311 |
+
**Phase A から学んだ悪設計のチェックリスト**
|
| 312 |
+
|
| 313 |
+
| 問題 | 影響 |
|
| 314 |
+
|---|---|
|
| 315 |
+
| gamma を grid search せず推測 | 50-500 倍ズレ |
|
| 316 |
+
| adv_weight=0.05 で「安全装置」になると誤解 | gradient norm は別軸、効かなかった |
|
| 317 |
+
| sanity 50 step だけで本番投入 | 緩慢な発散見逃し |
|
| 318 |
+
| `d_r1/r2` にアラートしない | 早期 abort 機会逸失 |
|
| 319 |
+
| 中間 ckpt で部分検証しなかった | 完走してから初めて発覚($70 ロス) |
|
| 320 |
+
|
| 321 |
+
### 4.2 Phase B v1 — Q,V LoRA だと AdaLN を触れない
|
| 322 |
+
|
| 323 |
+
R3GAN を外して安全側に振った構成。loss は 0.02-0.03 で安定したが、生成すると
|
| 324 |
+
**緑色化した anime キャラ**。LoRA strength を 1.0 → 0.3 に下げると base に戻る
|
| 325 |
+
(= LoRA は「均一に緑方向に押している」)。
|
| 326 |
+
|
| 327 |
+
原因: Q,V LoRA は cross-attention / self-attention の query / value だけを変更。
|
| 328 |
+
**timestep modulation の入り口である AdaLN modulation Linear を触れない**。
|
| 329 |
+
DiT が「少ない step でちゃんと denoise する」ために必要な timestep 解釈の変更が
|
| 330 |
+
できず、勾配が attention の color / style shift に逃げて degenerate に収束。
|
| 331 |
+
|
| 332 |
+
→ 解決: LoRA target を **「AdaLN + attention + MLP の全 Linear」(wide LoRA)** に拡張。
|
| 333 |
+
公式 Anima Turbo LoRA も同じ範囲を target にしている(検証で 1016 keys 確認)。
|
| 334 |
+
|
| 335 |
+
### 4.3 Phase B v2 — wide LoRA + R3GAN OFF で mean collapse
|
| 336 |
+
|
| 337 |
+
wide LoRA(980 keys)に拡張したら、今度は **完全に灰緑色の平面**を出力。loss は
|
| 338 |
+
v1 より遥かに小さく(`l_dmd` 0.001-0.007)、**訓練ログ上は最も健全に見える**
|
| 339 |
+
失敗。LoRA strength を変えても改善せず。
|
| 340 |
+
|
| 341 |
+
原因: DMD2 単体は「real distribution の方向」を示すだけ。LoRA capacity が大きい
|
| 342 |
+
ほど **「data distribution の mean を出力すれば DMD gradient はゼロになる」**
|
| 343 |
+
という degenerate な解に収束しやすい。R3GAN なしでは「sharp なものを出せ」と
|
| 344 |
+
gen に教える信号がないので、訓練が進むほど mean に潰れていく。
|
| 345 |
+
|
| 346 |
+
→ 解決: **R3GAN(または同等の reward / classifier loss)を必ず投入**。DMD2 系の
|
| 347 |
+
全公式実装(NVIDIA DMD2, Z-Image DMDR, Hyper-SD)で adv 系 loss が必須なのは
|
| 348 |
+
このため。
|
| 349 |
+
|
| 350 |
+
### 4.4 Phase B v3a — gamma=0.1 で D が即爆発
|
| 351 |
+
|
| 352 |
+
grid search の 100-200 step では gamma=0.1 が安定に見えていた:
|
| 353 |
+
```
|
| 354 |
+
[step 60/100] d_r1=0.020 d_r2=0.015 l_disc=0.017 ← 健全
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
しかし本番(2000 step)で開始 30 step 以内に爆発:
|
| 358 |
+
```
|
| 359 |
+
[step 10/2000] d_r1=67.5 d_r2=74.8 l_disc=7.1 ← 既に異常
|
| 360 |
+
[step 30/2000] d_r1=1648.0 d_r2=1821.6 ← guard 発火 abort
|
| 361 |
+
```
|
| 362 |
+
|
| 363 |
+
原因推測:
|
| 364 |
+
- 短時間 grid search は **ランダム初期化の運**で偶然安定 zone に居ただけ
|
| 365 |
+
- 本番の異なる初期化 + 長時間 → 不安定境界を越えた
|
| 366 |
+
- gamma=0.1 は Anima latent では **安定境界の崖際**
|
| 367 |
+
|
| 368 |
+
### 4.5 Phase B v3b — gamma=1.0 で長期 mean collapse + 最終 D 爆発
|
| 369 |
+
|
| 370 |
+
gamma=1.0 で training は確かに **1500 step まで全 metric 健全**:
|
| 371 |
+
```
|
| 372 |
+
[step 1000/2000] l_dmd=0.001 l_consist=0.005 d_r1=0.005 l_disc=0.004 ← 全部小さい
|
| 373 |
+
```
|
| 374 |
+
|
| 375 |
+
**しかし step 500/1000/1500 の中間 ckpt で生成テストすると全部 mean collapse**:
|
| 376 |
+
- step 500: 白い平面
|
| 377 |
+
- step 1000: 白い平面
|
| 378 |
+
- step 1500: 青+白の blob
|
| 379 |
+
|
| 380 |
+
しかも step 1380 から D が「目覚め」始め、step ~1600 で d_r1=2656 で abort:
|
| 381 |
+
```
|
| 382 |
+
[step 1380/2000] d_adv=3.73 d_r1=0.36 d_r2=0.22 ← D が動き始める
|
| 383 |
+
[step 1510/2000] d_adv=0.00 d_r1=0.67 d_r2=0.45 ← じわじわ拡大
|
| 384 |
+
[step ~1600] d_r1=2656.0 d_r2=3043.2 ← abort
|
| 385 |
+
```
|
| 386 |
+
|
| 387 |
+
**核心の理解**:
|
| 388 |
+
- gamma=1.0 は D が **静か → 突然動く → 爆発** のパターン
|
| 389 |
+
- 静かな間(d_r1 ~ 0.005)は **D が adv 信号を gen に渡せず**、gen は mean に collapse
|
| 390 |
+
- D が動き始めた頃には gen は既に degenerate state
|
| 391 |
+
- ckpt が「健全に見える」のは loss だけ。実態は破綻
|
| 392 |
+
|
| 393 |
+
これが **「R3GAN の安定 zone が存在しない」** という最終診断の根拠。
|
| 394 |
+
|
| 395 |
+
### 4.6 失敗パターンの直交性
|
| 396 |
+
|
| 397 |
+
| | Phase A | B v1 | B v2 | B v3a | B v3b |
|
| 398 |
+
|---|---|---|---|---|---|
|
| 399 |
+
| failure mode | gradient damage(ノイズ)| degenerate(緑) | mean collapse | D 爆発 abort | D 静止 → mean collapse → D 爆発 |
|
| 400 |
+
| 根本原因 | R3GAN γ 過大 | LoRA target 狭 | adv なし | γ 小すぎて D 暴走 | γ 大すぎて D が adv 信号出さず |
|
| 401 |
+
| 検出 | step 1000+ d_r1 | 生成テスト即 | 生成テスト即 | step 30 で guard | 中間 ckpt 生成テスト |
|
| 402 |
+
| 対策 | γ grid search + guard | wide LoRA | R3GAN を入れる | γ 上げる | γ 下げる(でも v3a に戻る)|
|
| 403 |
+
|
| 404 |
+
**5 つの failure mode は互いに直交かつ循環**(γ 下げると v3a、上げると v3b)。
|
| 405 |
+
Anima latent で R3GAN を安定させる γ は、**存在するとしても極めて狭く**、
|
| 406 |
+
我々の grid 解像度では発見できなかった。
|
| 407 |
+
|
| 408 |
+
### 4.7 周辺で踏んだ Modal / diffusion-pipe / ComfyUI 罠
|
| 409 |
+
|
| 410 |
+
蒸留の本体とは別に、インフラ側で踏んだバグ:
|
| 411 |
+
|
| 412 |
+
**diffusion-pipe を programmatic に使う際の 8 件**
|
| 413 |
+
|
| 414 |
+
| 罠 | 症状 | 対処 |
|
| 415 |
+
|---|---|---|
|
| 416 |
+
| `models/`, `utils/` が namespace package(`__init__.py` なし)| site-packages 側の `utils` に shadow されて `from utils.common import` 失敗 | `importlib.util.spec_from_file_location` で強制ロード(`scripts/distill/anima_loader.py`)|
|
| 417 |
+
| `CosmosPredict2Pipeline(cfg)` の dtype が **torch.dtype 必須** | `tensor() argument 'dtype' must be torch.dtype, not str` | `cfg["model"]["dtype"] = torch.bfloat16`(文字列禁止) |
|
| 418 |
+
| `load_text_encoder()` / `load_vae()` メソッドが **存在しない** | AttributeError | `load_diffusion_model()` だけ呼ぶ、残りは `__init__` で完了 |
|
| 419 |
+
| `MiniTrainDIT.forward` は `padding_mask` 必須(None 不可) | `transforms.functional.resize(None, ...)` TypeError | `torch.zeros(B, 1, H, W)` を渡す |
|
| 420 |
+
| VAE input が bfloat16 必須 | `Input type (float) and bias type (BFloat16) should be the same` | `vae_encode` 内で auto-cast |
|
| 421 |
+
| DiT 入力 `(noisy, t, cond)` の dtype 統一必須 | `expected mat1 and mat2 same dtype` | dit_forward helper で全部 weight dtype に揃える |
|
| 422 |
+
| `torchvision` import するが requirements に無い | `ModuleNotFoundError` | image に明示インストール |
|
| 423 |
+
| save の filter で integer buffer を落とすと壊れる | ロード後ノイズ | float 限定 filter は廃止、全 tensor 保存 |
|
| 424 |
+
|
| 425 |
+
**ComfyUI LoRA 形式**
|
| 426 |
+
|
| 427 |
+
Anima/Cosmos 用 LoRA は `lora_A.weight` / `lora_B.weight`(NOT `lora_down/up`)、
|
| 428 |
+
`diffusion_model.` prefix 必須。PEFT 出力からの変換は `modal_app.py::_convert_peft_to_comfy_lora`。
|
| 429 |
+
SDXL 流 `lora_down/up` に変換すると ComfyUI が key 認識できずランダム scaling で
|
| 430 |
+
適用 → 出力が緑化(これも Phase B v1 のもう 1 つの寄与因子だった可能性)。
|
| 431 |
+
|
| 432 |
+
**Modal 運用**
|
| 433 |
+
|
| 434 |
+
| 罠 | 対処 |
|
| 435 |
+
|---|---|
|
| 436 |
+
| `modal run --detach` の local CLI が早く戻る | 完了確認は `modal app list` / `modal app logs <id>` |
|
| 437 |
+
| `print()` の block buffering で logs 滞留 | `PYTHONUNBUFFERED=1` を env に追加 |
|
| 438 |
+
| Volume commit は subprocess 終了時のみ | 訓練ループ内で `modal.Volume.from_name(...).commit()` を定期発行 |
|
| 439 |
+
| `modal volume cp` は cross-volume 不可 | 両 volume mount した Modal function で `shutil.copy` |
|
| 440 |
+
| `run_in_background=true` + TaskStop で app が "detached_disconnected" に残る | 必ず `--detach` を一緒に付ける(明示 detach)|
|
| 441 |
+
| Anima ckpt は `net.` prefix 前提で ComfyUI が arch 検出 | 保存時に `{"net." + k: v for k, v in sd.items()}` |
|
| 442 |
+
|
| 443 |
+
### 4.8 コスト・時間の実測値
|
| 444 |
+
|
| 445 |
+
| タスク | 時間 | コスト |
|
| 446 |
+
|---|---|---|
|
| 447 |
+
| dataset 生成(B200 × 10 並列) | 40 分 | $44 |
|
| 448 |
+
| Path A baseline 確認(Turbo LoRA DL + 10 枚生成) | 5 分 | ~$1 |
|
| 449 |
+
| **Phase A: full DiT 訓練(失敗)** | **11.6 h** | **~$70**(全損)|
|
| 450 |
+
| Phase B v1: Q,V LoRA(失敗) | 1.6 h | ~$10 |
|
| 451 |
+
| Phase B v2: wide LoRA, R3GAN OFF(失敗) | 1.6 h | ~$10 |
|
| 452 |
+
| Phase B v3a: wide LoRA, γ=0.1(早期 abort) | ~5 分 | ~$5 |
|
| 453 |
+
| Phase B v3b: wide LoRA, γ=1.0(失敗) | 4 h | ~$20 |
|
| 454 |
+
| R3GAN gamma grid search | 30 分 | ~$3 |
|
| 455 |
+
| 比較生成 30 枚 + その他 | | ~$2 |
|
| 456 |
+
| Modal volume(50 GB × 月)| 常時 | ~$7.5/月 |
|
| 457 |
+
| **累計** | | **~$165 ≈ 25,000円** |
|
| 458 |
+
|
| 459 |
+
R3GAN ON で 1 step 約 +20-30%(D forward + R1/R2 backward 追加分)。
|
| 460 |
+
|
| 461 |
+
---
|
| 462 |
+
|
| 463 |
+
## 5. 続き
|
| 464 |
+
|
| 465 |
+
§4 の 5 回失敗を受け、「自前で書き下す」を諦めて **既存の動く実装を最小改造で
|
| 466 |
+
移植**した試行が 2026-05 から始まる。各手法 (Z-Image traj / LADD / Reflow / DMD2 /
|
| 467 |
+
SiD2 / Shortcut / PCM / DRaFT+) の移植ログ・コスト・結果は
|
| 468 |
+
[migration_log.md](migration_log.md) を参照。
|
docs/dmd2.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DMD2 (NVIDIA cosmos-predict2.5 流派) 蒸留
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md) — 関連: [PCM](pcm.md) / [DMDX](dmdx.md) / [蒸留全般](distillation.md)
|
| 4 |
+
|
| 5 |
+
## 概要
|
| 6 |
+
|
| 7 |
+
NVIDIA cosmos-predict2.5 公式の **DMD2 + TrigFlow** 流派を Anima 移植。同 base に **2 つの独立 PEFT adapter** (student / fake_score) を attach し `set_adapter()` で runtime 切替、TTUR (alt 5 critic : 1 generator)。
|
| 8 |
+
|
| 9 |
+
**実装**: `scripts/distill/{dmd2_official_loss,train_dmd2_official}.py`
|
| 10 |
+
**Modal**: `modal_app.py::train_dmd2_official_distill`
|
| 11 |
+
|
| 12 |
+
## 設計の鍵
|
| 13 |
+
|
| 14 |
+
- **2 adapter / single base** → memory 効率 (teacher は別 deepcopy)
|
| 15 |
+
- **TrigFlow time sampling**: `s ∈ shifted_uniform(shift=3-5) → t = arctan(s/(1-s))`
|
| 16 |
+
- **DMD2 gradient trick** (逆 KL): `grad = (x0_fake - x0_teacher) / |x_hat - x0_teacher|.mean()`
|
| 17 |
+
→ `loss = ((x_hat - (x_hat - grad).detach())**2).mean()`
|
| 18 |
+
- **Critic phase**: weighted denoise with `1/sin(t)**2` factor
|
| 19 |
+
- **few-step rollout** (n=1..4)、grad は最終 step のみ (memory efficient)
|
| 20 |
+
|
| 21 |
+
## 訓練設定 (実走、2026-05-18 - 19)
|
| 22 |
+
|
| 23 |
+
| 項目 | 値 |
|
| 24 |
+
|---|---|
|
| 25 |
+
| Base | `anima-base-v1.0.safetensors` |
|
| 26 |
+
| Method | DMD2 + TrigFlow (cosmos-predict2.5) |
|
| 27 |
+
| LoRA target | wide (980 keys) × 2 (student + fake_score) |
|
| 28 |
+
| LoRA rank | 32 |
|
| 29 |
+
| Total outer | 5000 (1 outer = 5 critic + 1 generator) |
|
| 30 |
+
| n_student_steps | 4 |
|
| 31 |
+
| Teacher CFG | **4.5** (Anima 公式推奨に整合、過去 preview3 試行の 3.0 から変更) |
|
| 32 |
+
| Student CFG | 1.0 (CFG embedded) |
|
| 33 |
+
| Shift | **3.0** (Anima sigma_shift と一致、過去の 5.0 から変更) |
|
| 34 |
+
| LR (gen / critic) | 5e-6 / 1e-5 (TTUR) |
|
| 35 |
+
| Warm-start | **`/models/loras/anima_turbo.safetensors`** (Civitai 公式 Turbo、968/1016 keys 一致) |
|
| 36 |
+
| GPU | B200 |
|
| 37 |
+
| 訓練時間 | step 660 まで ~40 min、resume + 4500 outer ~7h |
|
| 38 |
+
| コスト | 累計 ~$48-55 想定 (rate 6.2s/outer parallel 時) |
|
| 39 |
+
|
| 40 |
+
## 試行履歴 (2026-05-18 - 19)
|
| 41 |
+
|
| 42 |
+
### 1st run (`/output/dmd2_v1/`)
|
| 43 |
+
- 設定: warm-start (Anima Turbo) + 5000 outer
|
| 44 |
+
- 経過: step 660/5000 で `KeyboardInterrupt`
|
| 45 |
+
- 原因: **Modal workspace billing cycle spend limit reached** (DMDX 並列起動で予算限度超過)
|
| 46 |
+
- 保存: step 500 ckpt のみ無事 (volume commit 済)
|
| 47 |
+
|
| 48 |
+
### Resume (`/output/dmd2_v1_resume/`)
|
| 49 |
+
- 設定: step 500 ckpt を warm-lora、残り 4500 outer
|
| 50 |
+
- warm-load: **980/980 keys 完全一致** (自前 LoRA 同 architecture)
|
| 51 |
+
- 進行中、step 500 (= 元 1000 相当) / step 1000 (= 元 1500) で ckpt 保存
|
| 52 |
+
|
| 53 |
+
## Loss 推移 (順調)
|
| 54 |
+
|
| 55 |
+
| outer | l_dmd_critic | l_dmd_gen | 評価 |
|
| 56 |
+
|---|---|---|---|
|
| 57 |
+
| 0 | 0.0364 | 1.5780 | warm-start 直後 |
|
| 58 |
+
| 100 | 0.07 | 1.18 | gen 急速降下中 |
|
| 59 |
+
| 500 | 0.06 | 1.30 | spike あるが healthy |
|
| 60 |
+
| 660 (中断) | 0.0471 | 0.16 | 健全な状態で中断 |
|
| 61 |
+
| resume 0 | 0.0364 | 1.5771 | warm-load から再開、原 outer 0 と類似 |
|
| 62 |
+
| resume 500 | 0.058 | 1.29 | 期待通り |
|
| 63 |
+
| resume 1000 | 0.054 | 0.89 | 順調収束 |
|
| 64 |
+
|
| 65 |
+
magnitude (x_hat / teacher / fake): ~0.3-0.5 範囲で揃って distill 方向収束。
|
| 66 |
+
|
| 67 |
+
## 検証結果 (step 500、4-step / er_sde+simple)
|
| 68 |
+
|
| 69 |
+
サンプル画像: `/dataset/ckpt_health_check/dmd2_v1_step00500_step4_cfg1.0/p0000_s000.png`
|
| 70 |
+
|
| 71 |
+
- 2 キャラ (Flandre / Remilia) 正確分離
|
| 72 |
+
- Civitai Turbo に近い品質 (warm-start 支配的、step 500 で大幅変化なし)
|
| 73 |
+
- 生成時間 10.95s (4-step、CFG 1.0、B200 sageattention)
|
| 74 |
+
|
| 75 |
+
ローカル DL 済: `~/Downloads/anima_loras/dmd2/dmd2_v1_step00500.safetensors`
|
| 76 |
+
|
| 77 |
+
## 配布
|
| 78 |
+
|
| 79 |
+
完走後 HF `darask0/anima-distill-loras/dmd2/` に upload 予定。
|
| 80 |
+
|
| 81 |
+
## ComfyUI 推奨設定
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
LoraLoaderModelOnly:
|
| 85 |
+
lora_name: dmd2_v1_step00500.safetensors
|
| 86 |
+
strength_model: 1.0
|
| 87 |
+
|
| 88 |
+
ModelSamplingAuraFlow:
|
| 89 |
+
shift: 3.0
|
| 90 |
+
|
| 91 |
+
KSampler:
|
| 92 |
+
steps: 4
|
| 93 |
+
cfg: 1.0
|
| 94 |
+
sampler_name: er_sde
|
| 95 |
+
scheduler: simple
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## 過去の preview3 試行との対比
|
| 99 |
+
|
| 100 |
+
[migration_log.md §5.7](migration_log.md#57-実画像比較-2026-05同-prompt--seed) の preview3 DMD2 では:
|
| 101 |
+
- 8-step: 絵画調シフト、洗色傾向
|
| 102 |
+
- 4-step: シルエットのみ、ボケすぎ
|
| 103 |
+
|
| 104 |
+
→ 改善要因 (v1.0 試行):
|
| 105 |
+
- ✅ **warm-start (Civitai Turbo)** 必須化
|
| 106 |
+
- ✅ **teacher_cfg 3.0 → 4.5** (Anima 公式推奨)
|
| 107 |
+
- ✅ **shift 5.0 → 3.0** (Anima sigma_shift 整合、sampler 互換性向上)
|
| 108 |
+
- ✅ v1.0 base 直接 (preview3 ベース LoRA の style drift 回避)
|
| 109 |
+
|
| 110 |
+
## 知見
|
| 111 |
+
|
| 112 |
+
- **2 adapter peft set_adapter() で必須**: 毎 forward で明示切替しないと片方の重みが leak ([operations.md](operations.md) 参照)
|
| 113 |
+
- **`No module named 'modal'` の volume commit 失敗**: subprocess 内で modal lib 未参照、harmless warning (function 終了時に Modal 自動 commit)
|
| 114 |
+
- **billing limit による KeyboardInterrupt**: Modal の workspace spend limit に達すると **cloud function に SIGINT** が送られて停止する。ローカル CLI の interrupt とは別経路、Modal dashboard で billing を確認・上げる必要
|
| 115 |
+
- **parallel rate 低下**: 並列 (DMDX と) で 3.66s/outer → 6.2s/outer (~1.7x 遅い)、別 B200 アロケート時の��雑度に依存
|
docs/dmdx.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DMDX (Adversarial Distribution Matching) 蒸留
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md) — 関連: [PCM](pcm.md) / [DMD2](dmd2.md) / [蒸留全般](distillation.md)
|
| 4 |
+
|
| 5 |
+
## 論文
|
| 6 |
+
|
| 7 |
+
**"Adversarial Distribution Matching for Diffusion Distillation Towards Efficient Image and Video Synthesis"**
|
| 8 |
+
- arxiv: [2507.18569v1](https://arxiv.org/html/2507.18569v1)
|
| 9 |
+
- 著者: Yanzuo Lu et al. (Sun Yat-Sen University + ByteDance Seed Vision)
|
| 10 |
+
- 公開コード: なし (論文のみ)
|
| 11 |
+
|
| 12 |
+
## 核心アイデア
|
| 13 |
+
|
| 14 |
+
DMD2 の `(real_score - fake_score)` の **逆 KL 発散** はモード崩壊しがち (Phase B v2 と同症状)。DMDX は **学習可能 discriminator による hinge GAN (TVD = Total Variation Distance、対称) 最小化** に置換し、根本解決を狙う。
|
| 15 |
+
|
| 16 |
+
### vs DMD2
|
| 17 |
+
|
| 18 |
+
| 観点 | DMD2 | DMDX (ADM) |
|
| 19 |
+
|---|---|---|
|
| 20 |
+
| 発散尺度 | 逆 KL (非対称、mean collapse 寄り) | **TVD (対称)** |
|
| 21 |
+
| 損失実装 | grad trick で score difference 直接最小化 | **学習可能 discriminator** で hinge loss 敵対学習 |
|
| 22 |
+
| GAN 役割 | optional 正則化 | **本体損失** |
|
| 23 |
+
| 時刻情報 | t での score 比較のみ | `t → t-Δt` の ODE 進化中間点も考慮 |
|
| 24 |
+
| Initialization | MSE pretrain | 論文では ADP (= adversarial pre-training)、本実装は warm-start で代替 |
|
| 25 |
+
|
| 26 |
+
## 本実装の範囲
|
| 27 |
+
|
| 28 |
+
論文の **ADM のみ移植**、**ADP は省略** (SAM 依存 pixel-space discriminator が重い、コスト効果が低い)。
|
| 29 |
+
|
| 30 |
+
代替: **Civitai Anima Turbo を warm-start** で「分布レベルの良い初期化」を確保。
|
| 31 |
+
|
| 32 |
+
**実装**: `scripts/distill/{dmdx_loss,train_dmdx}.py`
|
| 33 |
+
**Modal**: `modal_app.py::train_dmdx_distill`
|
| 34 |
+
|
| 35 |
+
### ADM の構造
|
| 36 |
+
|
| 37 |
+
- **Discriminator backbone** = teacher MiniTrainDIT (frozen) — **LADD の `AnimaLADDDiscriminator` を流用**
|
| 38 |
+
- **Trainable heads** × 5 (blocks 2/8/14/20/26 を hook、各 spectral norm + BatchNormLocal)
|
| 39 |
+
- **Cubic time schedule**: `u ~ U(0,1), t = 1 - u**3` (high-noise バイアス)
|
| 40 |
+
- **Δt evolution**: teacher で 1-step Euler で `t → t-Δt` 進化、Δt = T/64
|
| 41 |
+
- **Hinge loss**: G = `mean(-D(fake))`、D = `relu(1-D(real)) + relu(1+D(fake))`
|
| 42 |
+
|
| 43 |
+
## 訓練設定 (実走、2026-05-19)
|
| 44 |
+
|
| 45 |
+
| 項目 | 値 |
|
| 46 |
+
|---|---|
|
| 47 |
+
| Base | `anima-base-v1.0.safetensors` |
|
| 48 |
+
| Method | DMDX ADM-only |
|
| 49 |
+
| Student LoRA | wide (980 keys、single adapter) |
|
| 50 |
+
| LoRA rank | 32 |
|
| 51 |
+
| Disc heads | 5 (block 2/8/14/20/26)、各 ~3.4M = 17.1M total |
|
| 52 |
+
| Total outer | 5000 (1 outer = 2 disc + 1 gen) |
|
| 53 |
+
| n_critic_per_gen | **2** (DMD2 の 5 より少なめ、hinge GAN 系は 1:1〜2:1 が一般的) |
|
| 54 |
+
| n_student_steps | 4 |
|
| 55 |
+
| Teacher CFG | 4.5 |
|
| 56 |
+
| Student CFG | 1.0 |
|
| 57 |
+
| dt_ratio | 1/64 ≈ 0.0156 |
|
| 58 |
+
| recon_weight | **0.0** (pure ADM、LADD の Smooth-L1 anchor なし) |
|
| 59 |
+
| LR (gen / disc) | 5e-6 / 1e-5 |
|
| 60 |
+
| Warm-start | **`/models/loras/anima_turbo.safetensors`** (Civitai 公式 Turbo) |
|
| 61 |
+
| GPU | B200 |
|
| 62 |
+
| 訓練時間 | smoke ~35s (10 outer)、本番 ~5h 想定 (3.6s/outer) |
|
| 63 |
+
| コスト | smoke ~$0.5、本番 ~$30 想定 |
|
| 64 |
+
|
| 65 |
+
## Smoke 結果 (10 outer、2026-05-19)
|
| 66 |
+
|
| 67 |
+
| outer | l_d_real | l_d_fake | l_d_total | l_g_adv | 評価 |
|
| 68 |
+
|---|---|---|---|---|---|
|
| 69 |
+
| 0 | 0.717 | 1.295 | 2.012 | -0.292 | 初期 hinge equilibrium |
|
| 70 |
+
| 5 | 0.724 | 1.277 | 2.001 | -0.276 | 安定継続 |
|
| 71 |
+
| 9 | 0.740 | 1.268 | 2.008 | -0.270 | smoke 完了 |
|
| 72 |
+
|
| 73 |
+
OOM / NaN / divergence なし。setup・warm-start (968/1016)・D init・ADM loss 全部正常。
|
| 74 |
+
|
| 75 |
+
## 本番訓練 (進行中、2026-05-19)
|
| 76 |
+
|
| 77 |
+
### Step 500 検証
|
| 78 |
+
|
| 79 |
+
| outer | l_d_real | l_d_fake | l_d_total | l_g_adv |
|
| 80 |
+
|---|---|---|---|---|
|
| 81 |
+
| 0 | 0.717 | 1.295 | 2.012 | -0.292 |
|
| 82 |
+
| 90 | 0.774 | 1.227 | 2.001 | -0.218 |
|
| 83 |
+
| 340 | 0.938 | 1.034 | 1.972 | +0.048 (D 優勢) |
|
| 84 |
+
| **500** | **0.822** | **0.976** | **1.798** | **-0.146** |
|
| 85 |
+
| 1000 | 0.933 | 0.971 | 1.904 | -0.093 |
|
| 86 |
+
|
| 87 |
+
GAN dynamics:
|
| 88 |
+
- `l_d_total` 2.012 → 1.798 → 1.904 (D 学習、後半は equilibrium 周辺で振動)
|
| 89 |
+
- `l_g_adv` -0.292 → +0.05 (一時 D 優勢) → -0.15 → -0.09 (G 持ち直し)
|
| 90 |
+
- D と G の拮抗状態に到達、healthy GAN training
|
| 91 |
+
|
| 92 |
+
### 生成検証 (step 500、4-step / er_sde+simple)
|
| 93 |
+
|
| 94 |
+
サンプル: `/dataset/ckpt_health_check/dmdx_v1_step00500_step4_cfg1.0/p0000_s000.png`
|
| 95 |
+
|
| 96 |
+
- 2 キャラ正確分離、Civitai Turbo に近い品質 (warm-start 支配)
|
| 97 |
+
- 生成時間 **5.77s** (PCM 8.8s / DMD2 11s より速い、B200 contention 軽い時間帯)
|
| 98 |
+
|
| 99 |
+
## 配布
|
| 100 |
+
|
| 101 |
+
完走後 HF `darask0/anima-distill-loras/dmdx/` に upload 予定。
|
| 102 |
+
|
| 103 |
+
## ComfyUI 推奨設定
|
| 104 |
+
|
| 105 |
+
```
|
| 106 |
+
LoraLoaderModelOnly:
|
| 107 |
+
lora_name: dmdx_student_final_comfy.safetensors
|
| 108 |
+
strength_model: 1.0
|
| 109 |
+
|
| 110 |
+
ModelSamplingAuraFlow:
|
| 111 |
+
shift: 3.0
|
| 112 |
+
|
| 113 |
+
KSampler:
|
| 114 |
+
steps: 4
|
| 115 |
+
cfg: 1.0
|
| 116 |
+
sampler_name: er_sde
|
| 117 |
+
scheduler: simple
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## 期待品質 (paper 報告ベース)
|
| 121 |
+
|
| 122 |
+
SDXL 1-step での DMDX vs DMD2:
|
| 123 |
+
|
| 124 |
+
| metric | DMD2 | DMDX | 差 |
|
| 125 |
+
|---|---|---|---|
|
| 126 |
+
| CLIP Score | 35.22 | 35.26 | +0.04 |
|
| 127 |
+
| PickScore | 22.10 | 22.27 | +0.17 |
|
| 128 |
+
| HPSv2 | 27.45 | 27.70 | +0.25 |
|
| 129 |
+
| MPS | 10.69 | 11.20 | +0.51 |
|
| 130 |
+
| **多様性 LPIPS** | 0.6715 | **0.7156** | **+0.044** |
|
| 131 |
+
|
| 132 |
+
→ **多様性 +6.5% が最大の利点**、画像品質は誤差レベル。Video (CogVideoX) では 8-step で 100-step 比 92-96% 加速の大きな成果。本実装は image only。
|
| 133 |
+
|
| 134 |
+
## 知見
|
| 135 |
+
|
| 136 |
+
- **LADD discriminator の再利用**: spectral norm + BatchNormLocal + multi-block hook の構造はそのまま流用可能、DMDX 専用 D を新規開発する必要なし
|
| 137 |
+
- **cubic time schedule**: shifted uniform より extreme な high-noise バイアス、`t_mean=0.0259-0.99` の wide range (cubic の variance 大)
|
| 138 |
+
- **n_critic_per_gen 削減**: DMD2 の 5 から **2** に。hinge GAN は D vs G の balance が critical で、disc 過多だと G が学習機会失う
|
| 139 |
+
- **`recon_weight=0` で開始**: pure ADM の純粋性 verify。不安定なら LADD 流 Smooth-L1 anchor (`>0`) で stability boost 可能
|
| 140 |
+
- **rate 3.6s/outer**: DMD2 の 3.7s と同等以下 (n_critic 削減効果)、parallel でも slowdown 小さい
|
| 141 |
+
- **billing limit 再発防止**: 並列 launch 前に Modal spend cycle limit を確認 (DMD2 v1 が KeyboardInterrupt'd した先例あり)
|
docs/migration_log.md
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 既存実装の移植 — 2026-05 試行ログ
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md) — 前章: [distillation.md](distillation.md)
|
| 4 |
+
|
| 5 |
+
> ⚠️ 本ドキュメントは **2026-05-17 時点のスナップショット**を含む。Modal volume の
|
| 6 |
+
> 状態 (LoRA path、cache 内容など) は時間とともに変動するので、最新状態は
|
| 7 |
+
> `modal volume ls` で確認すること。配布済 LoRA は HF の
|
| 8 |
+
> [darask0/anima-distill-loras](https://huggingface.co/darask0/anima-distill-loras) に集約。
|
| 9 |
+
|
| 10 |
+
[§4 失敗分析](distillation.md#4-実際の失敗全-5-件詳細) の 5 回失敗を受け、「自前で書き下す」のを諦めて **既存の動く実装を最小改造で
|
| 11 |
+
Anima に移植する** 方針に転換。survey と smoke test を経て **5 手法** を実装、
|
| 12 |
+
本リポジトリに収録 (`scripts/distill/`)。
|
| 13 |
+
|
| 14 |
+
## 5.0 全体マップ
|
| 15 |
+
|
| 16 |
+
```
|
| 17 |
+
┌── Z-Image trajectory imitation (DiffSynth-Studio 由来)
|
| 18 |
+
├── DMD2 + TrigFlow (NVIDIA cosmos-predict2.5 公式)
|
| 19 |
+
蒸留 5 候補 ────┼── LADD (AMD Nitro-1 移植、PixArt 由来)
|
| 20 |
+
├── Reflow / InstaFlow (rfpp NeurIPS 2024 知見適用)
|
| 21 |
+
└── PCM (G-U-N/Phased-Consistency-Model、SD3 変種)
|
| 22 |
+
|
| 23 |
+
無料補完 ────── ComfyUI-CapitanFlowMatch / ZiT-Scheduler (推論側 scheduler)
|
| 24 |
+
|
| 25 |
+
survey で却下 ── DMDR / Hyper-SD / NitroFusion / ADD (訓練コード非公開)
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## 5.1 採用 5 手法の概要
|
| 29 |
+
|
| 30 |
+
| # | 手法 | カテゴリ | 中核ファイル | 設計の鍵 |
|
| 31 |
+
|---|---|---|---|---|
|
| 32 |
+
| ① | Z-Image trajectory imitation | trajectory matching | `scripts/distill/{traj_scheduler,traj_loss,train_traj}.py` | critic なし、teacher 50-step CFG=2 trajectory を student 8-step に align |
|
| 33 |
+
| ② | DMD2 + TrigFlow (cosmos-predict2.5) | score matching | `scripts/distill/{dmd2_official_loss,train_dmd2_official}.py` | **同じ base に 2 LoRA adapter** (student + fake_score)、`set_adapter()` 切替、alt 5:1 |
|
| 34 |
+
| ③ | LADD (AMD Nitro-1) | adversarial + recon | `scripts/distill/{anima_ladd_disc,train_ladd}.py` + `precompute_teacher_x0.py` | D backbone = teacher MiniTrainDIT (frozen)、5 spectral-norm head、Smooth-L1 recon anchor |
|
| 35 |
+
| ④ | Reflow / InstaFlow | flow alignment | `scripts/distill/train_reflow.py` (cache は `--save-noise` 必須) | Anima は元から RF、U-shape t + Huber + 間欠 LPIPS、1 grad-through で最安 |
|
| 36 |
+
| ⑤ | PCM (Phased Consistency Model) | consistency | `scripts/distill/{pcm_scheduler,train_pcm}.py` | SD3-PCM 流派 (FlowMatch + v-pred)、Anima RF と math 一致、N=50 を K=4 phase に分割 |
|
| 37 |
+
|
| 38 |
+
全手法に共通の前提:
|
| 39 |
+
- **wide LoRA** (`attach_wide_lora()` で AdaLN + attn + MLP 全 Linear、454 modules、~72M trainable)
|
| 40 |
+
- **warm-start = Civitai 公式 Anima Turbo LoRA** (matched 968/1016 keys、x_embedder/t_embedder の 12 keys のみ skip)
|
| 41 |
+
- **B200 GPU** + dataset は `/dataset/raw` (5000 caption、品質タグは未掃除でも蒸留には影響なし)
|
| 42 |
+
|
| 43 |
+
## 5.2 R3GAN との決定的な違い (なぜ LADD は動く可能性が高いか)
|
| 44 |
+
|
| 45 |
+
| 失敗パターン | R3GAN | LADD |
|
| 46 |
+
|---|---|---|
|
| 47 |
+
| D が無信号 → mean collapse | CNN を一から学習、16ch latent prior なし | D backbone = **teacher MiniTrainDIT (frozen)**、Anima latent を既に理解 |
|
| 48 |
+
| D 爆発 | γ (gradient penalty) calibration 困難 | **spectral norm + BatchNormLocal** で D の Lipschitz 制約 |
|
| 49 |
+
| anchor 不在で発散 | DMD2 単独だと mean に潰れる | **Smooth-L1 recon loss** (vs teacher x0) で「絶対に teacher から離れない」床を作る |
|
| 50 |
+
|
| 51 |
+
LADD smoke (5 step、`l_g_adv ≈ ln(2) ≈ 0.69`, `l_d_total ≈ 2·ln(2) ≈ 1.39`) — R3GAN 試行で
|
| 52 |
+
1 度も達成できなかった「**stable initialization**」が初手で出る。
|
| 53 |
+
|
| 54 |
+
## 5.3 LoRA-only + warm-start + precompute cache の三位一体
|
| 55 |
+
|
| 56 |
+
3 つを組み合わせて実コストを大幅圧縮:
|
| 57 |
+
|
| 58 |
+
1. **LoRA-only (~72M trainable / 2B base)** — gen と D backbone は frozen、optimizer state も小さい
|
| 59 |
+
2. **warm-start (Civitai Turbo LoRA)** — cold-start より convergence が早く、品質下限が保証される
|
| 60 |
+
3. **precompute teacher x0 cache** — teacher の 20-step CFG=4.5 rollout を 1 度だけ実行 (~$11 / 5000 サンプル)、
|
| 61 |
+
その後の蒸留で **複数手法が同じ cache を共有** (LADD / PCM が利用)
|
| 62 |
+
|
| 63 |
+
```
|
| 64 |
+
precompute_teacher_x0_cache (1回, $11)
|
| 65 |
+
│
|
| 66 |
+
├── LADD train (cache の x0 を real として使用)
|
| 67 |
+
└── PCM train (cache の emb を流用、x0 は teacher rollout source)
|
| 68 |
+
|
| 69 |
+
precompute_for_reflow (--save-noise 付き、別 cache, $11)
|
| 70 |
+
│
|
| 71 |
+
└── Reflow train (noise + x0 + emb triplet)
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 5.4 採用しなかった手法 (survey 結果)
|
| 75 |
+
|
| 76 |
+
| 手法 | 却下理��� |
|
| 77 |
+
|---|---|
|
| 78 |
+
| **DMDR** (vvvvvjdy/dmdr) | ImageNet/SiT のクラス条件 toy のみ公開、T2I (Z-Image, SD3.5) パイプラインは未公開 |
|
| 79 |
+
| **Hyper-SD / Hyper-FLUX** (ByteDance) | 訓練コード非公開 (HF model card のみ)、コミュニティ port なし |
|
| 80 |
+
| **NitroFusion / NitroSD-Vibrant** (ChenDarYen) | 訓練コード "Coming Soon" のまま停滞、リポジトリは 6 commit で凍結 |
|
| 81 |
+
| **ADD / LADD-DINOv2** (StabilityAI) | 訓練コード非公開 (issues wontfix)、AMD 版で代替 |
|
| 82 |
+
|
| 83 |
+
## 5.5 inference 側補完: AYS / TeaCache / GGUF / sageattention の適用可否
|
| 84 |
+
|
| 85 |
+
| 手法 | Anima 適用可否 | 理由 |
|
| 86 |
+
|---|---|---|
|
| 87 |
+
| **AYS** (NVIDIA Align Your Steps) | ❌ | ComfyUI 公式実装は **SD1/SDXL/SVD の sigma schedule を hardcode** で rectified flow 未対応 |
|
| 88 |
+
| **TeaCache** (timestep 残差 cache) | ⚠️ **base 高 step 用のみ** | 4-8 step 蒸留 LoRA とは**構造的に non-compatible** (連続 step の差分が大きすぎてスキップ不可)。base 28-50 step での **2x 速** には使える、porting ~250 LoC |
|
| 89 |
+
| **ComfyUI-GGUF** Q8/Q4 量子化 | ⚠️ Anima 未対応 | FLUX/SD3/Hunyuan 系のみ。Anima loader 追加 + GGUF 変換が必要 |
|
| 90 |
+
| **CapitanFlowMatch / CapitanZiT-Scheduler** | ✅ | rectified flow / Cosmos-Predict2 兄弟向け scheduler、ComfyUI ノード追加だけ |
|
| 91 |
+
| **sageattention** | ✅ | torch SDPA 置換で 1.2-1.5x 推論速、`pip install sageattention` で `_base_image` に同梱済 |
|
| 92 |
+
| **DPM-Solver++ 高次** / **UniPC** | ✅ | sampler ノード切替のみ、Anima 標準 `er_sde` の代替 |
|
| 93 |
+
|
| 94 |
+
**実用推奨**: 蒸留 LoRA + sageattention + CapitanFlowMatch scheduler のスタックが最も効率的。
|
| 95 |
+
TeaCache は「LoRA 使わず base 高品質モード」用の補完として将来 porting する価値あり (~250 LoC, ~$10 calibration)。
|
| 96 |
+
|
| 97 |
+
## 5.6 これから挑む人へ
|
| 98 |
+
|
| 99 |
+
- **survey をサボらない**: 4 つの「有名手法」が実は訓練コード非公開で詰む。GitHub の README を見るだけでなく `train.py` / `run.sh` を実際に開いて確認
|
| 100 |
+
- **`scripts/distill/anima_loader.py` は再利用** — `build_anima()` + `AnimaBundle` で diffusion-pipe の罠 (namespace shadow、`padding_mask` 必須、dtype object vs str など) を全部吸収済
|
| 101 |
+
- **smoke test を必ず 1 step 走らせる** — sign / dtype / 配線ミスを **$1-2 で発見**、本番 $30-50 をドブに捨てるリスク回避
|
| 102 |
+
- **`MSYS_NO_PATHCONV=1 PYTHONIOENCODING=utf-8 PYTHONUTF8=1` を modal CLI に毎回つける** (Windows JP locale 必須)
|
| 103 |
+
|
| 104 |
+
## 5.7 実画像比較 (2026-05、同 prompt × seed)
|
| 105 |
+
|
| 106 |
+
5 prompt × 2 seed × {8 step, 4 step} × {base CFG=4.5, Civitai 公式 Turbo CFG=1, ① Z-Image
|
| 107 |
+
trajectory imitation CFG=1, ② DMD2 CFG=1} = **80 枚** を Modal `compare_distill_loras` で
|
| 108 |
+
生成 + v1.0 base 上で **13 条件 1 prompt 1 seed** を `verify_completed_loras` で生成。
|
| 109 |
+
同 noise seed なので 1:1 で比較可能 (実装: [modal_app.py](../modal_app.py))。
|
| 110 |
+
|
| 111 |
+
**観察された明確な順位** (8-step CFG=1、preview3 base、5 prompt × 2 seed = 80 枚):
|
| 112 |
+
|
| 113 |
+
| Rank | 手法 | 評価 |
|
| 114 |
+
|---|---|---|
|
| 115 |
+
| 🥇 | **Civitai 公式 Anima Turbo** | シャープ、彩度高、盾の紋章/松明の光まで完備、キャラ識別正確 |
|
| 116 |
+
| 🥈 | ① Z-Image traj imitation (本 repo) | 鮮明・安定、ディテール OK |
|
| 117 |
+
| 🥉 | ② DMD2 (本 repo) | 絵画調シフト、やや洗色傾向 |
|
| 118 |
+
| ❌ | base 30step→8step | 暗め・シンプル、品質劣化大 |
|
| 119 |
+
|
| 120 |
+
**4-step での頑健性順位**:
|
| 121 |
+
|
| 122 |
+
| Rank | 手法 | 評価 |
|
| 123 |
+
|---|---|---|
|
| 124 |
+
| 🥇 | **Civitai 公式 Anima Turbo** | 8-step とほぼ同等品質を維持 |
|
| 125 |
+
| 🥈 | ① Z-Image | エッジ甘くなる (8-step 訓練だったため) |
|
| 126 |
+
| ❌ | ② DMD2 | シルエットのみ、ボケすぎ |
|
| 127 |
+
| ❌ | base | ほぼ崩壊、使用不可 |
|
| 128 |
+
|
| 129 |
+
**v1.0 base での生成時間 (sageattention 有効、1024×1024、er_sde/simple/shift=3)**:
|
| 130 |
+
|
| 131 |
+
| step / 条件 | 時間 | 備考 |
|
| 132 |
+
|---|---|---|
|
| 133 |
+
| 4-step LoRA (全 6 LoRA) | **3.5-4.0s** | 公式 Turbo / ① Z-Image / ② DMD2 / ④ Reflow / ⑩ DRaFT+ 系すべて |
|
| 134 |
+
| 8-step LoRA (全 6 LoRA) | **4.0-4.7s** | 同上 |
|
| 135 |
+
| 30-step base CFG=4.5 | **10.7s** | LoRA 無効、品質基準 |
|
| 136 |
+
|
| 137 |
+
→ **4-step LoRA = base 30 step の 3 倍速**、sageattention 効果込み。Modal B200 のコスト換算で
|
| 138 |
+
1 画像 ~$0.006 (4-step LoRA) vs ~$0.018 (base 30 step)。
|
| 139 |
+
|
| 140 |
+
**正直な結論**: **CircleStone Labs 公式 Turbo (Civitai) に予算 ~$300 で勝つのは現状困難**。
|
| 141 |
+
公式は遥かに多い GPU 時間と専門知見を投入した結果。本 repo の自前蒸留は:
|
| 142 |
+
|
| 143 |
+
- base からの **品質向上は明白** (8-step CFG=1 が実用レベル)
|
| 144 |
+
- 公式 Turbo に **代替/補完** できる位置 (ライセンス遵守、自前カスタマイズ可)
|
| 145 |
+
- **学習目的・特定スタイル特化・蒸留パイプライン理解** には十分価値
|
| 146 |
+
|
| 147 |
+
### 5.7.1 v1.0 base 切替で観察された style drift
|
| 148 |
+
|
| 149 |
+
2026-05-17 に base を `anima-preview3-base.safetensors` → `anima-base-v1.0.safetensors` に
|
| 150 |
+
切替。**既存 LoRA はすべて preview3 で訓練済だが v1.0 でも動作する** (アーキ互換)。ただし:
|
| 151 |
+
|
| 152 |
+
- **出力 style がデフォルメ寄りに変化** — verify_v1_sage の 13 条件すべてで chibi/SD 風が強く
|
| 153 |
+
出た。preview3 base での同じ LoRA の出力 (compare_z_vs_dmd2) は比較的写実寄り
|
| 154 |
+
- 原因仮説: v1.0 base が「young girl appearance」prompt を v1.0 の aesthetic prior でより
|
| 155 |
+
強く解釈する。preview3 とは prior 分布が異なる
|
| 156 |
+
- LoRA を再訓練するなら v1.0 base 上で行うのが筋。preview3 ベース LoRA を v1.0 で使う場合は
|
| 157 |
+
style ドリフトを受け入れる前提
|
| 158 |
+
|
| 159 |
+
## 5.8 まだ試せる方向 (諦める理由はない)
|
| 160 |
+
|
| 161 |
+
公式 Turbo に勝てないと判明しても、以下は試す価値がある:
|
| 162 |
+
|
| 163 |
+
1. **訓練 step を増やす** — ① Z-Image を 2000 → 8000 step に延長 (+$54)、loss はまだ
|
| 164 |
+
plateau の手前で振動していた
|
| 165 |
+
2. ~~**蒸留 LoRA に DRaFT+ HPSv2 reward を追加学習**~~ — **検証済、推奨せず** (§5.10 参照)。
|
| 166 |
+
HPSv2 score は +13 上昇したが視覚的には **reward hacking で劣化** (anime → 西洋イラスト
|
| 167 |
+
風にドリフト、キャラ固有要素曖昧化、prompt adherence 落下)
|
| 168 |
+
3. **より高解像度 teacher rollout** — 768 → 1024 で precompute (+$22 / cache、+$20 / train)
|
| 169 |
+
4. **スタイル/キャラ特化蒸留** — 公式 Turbo は汎用向け、特定ドメイン (例: 単一キャラ、
|
| 170 |
+
特定 artist style) なら自前のほうが優位の可能性
|
| 171 |
+
5. **LoRA rank を上げる** — 32 → 64 / 128 (`--lora-rank 64`)、より大きな容量で
|
| 172 |
+
teacher の細部を学べる可能性
|
| 173 |
+
6. **既存実装 (LADD/Reflow/PCM/SiD2/Shortcut) の本番結果を待つ** — 進行中、Z-Image を
|
| 174 |
+
超える手法が出る可能性は残る
|
| 175 |
+
7. **8 手法の **アンサンブル** — 複数 LoRA を runtime で blend (`ComfyUI` で
|
| 176 |
+
`LoraLoaderModelOnly` を 2 個直列に挟むだけ)、相補的に効くケースあり
|
| 177 |
+
8. **v1.0 base で再訓練** — 既存 LoRA は preview3 起点、v1.0 で訓練し直すと aesthetic
|
| 178 |
+
一致でさらに伸びる可能性 (§5.7.1 参照)
|
| 179 |
+
|
| 180 |
+
「公式に勝つ」のではなく「**公式とは違う強みを持つ自前 LoRA**」を作る方が現実解。
|
| 181 |
+
本 repo の 7 実装は **どれもライセンス上自由に派生可能** で、warm-start の柔軟性が
|
| 182 |
+
あるのが公式 Turbo にはない利点。
|
| 183 |
+
|
| 184 |
+
### 5.8.1 ファインチューニング dataset 生成 (進行中、2026-05-17)
|
| 185 |
+
|
| 186 |
+
**目的**: hakushiMixAnima_v02 base + anima-highres-aesthetic-boost LoRA を組み合わせて、
|
| 187 |
+
**特定 4 artist のスタイル mix LoRA** を fine-tune するための合成 dataset を生成。
|
| 188 |
+
|
| 189 |
+
**Artist tags (固定 4 名、本 repo では秘匿 ♥)**:
|
| 190 |
+
|
| 191 |
+
```
|
| 192 |
+
@artist_1, @artist_2, @artist_3, @artist_4
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
- **本ドキュメントではセキュリティ / プライバシーの観点から具体的 artist 名を伏せる**
|
| 196 |
+
- 実 artist tag は `scripts/finetune_prompts*.txt` (Modal volume の private prompts ファイル) と
|
| 197 |
+
user の脳内にのみ存在する
|
| 198 |
+
- **Claude (AI assistant) が処理上 artist tag の実値が必要になった場合は、必ず user に質問**
|
| 199 |
+
して聞き出すこと。推測や前回会話履歴からの引用で済まそうとしない (privacy 保護)
|
| 200 |
+
- 4 artist は毎 prompt 必ず含まれる (artist drop なし、style mix を強固に学習させる狙い)
|
| 201 |
+
|
| 202 |
+
**Dataset 構成**:
|
| 203 |
+
|
| 204 |
+
| subdir | safety | themes | seeds | 想定枚数 | 内容 |
|
| 205 |
+
|---|---|---|---|---|---|
|
| 206 |
+
| `/dataset/finetune_raw/` | `safe` | 60 | 16 | 960 | SFW v1: 単体キャラ + couple + group + 環境 |
|
| 207 |
+
| `/dataset/finetune_raw_v2/` | `safe` | 60 | 16 | 960 | SFW v2: animal/mytho/sports/cyberpunk/etc |
|
| 208 |
+
| `/dataset/finetune_raw_v3/` | `safe` | 60 | 16 | 960 | SFW v3: 職業/世界観 (idol/detective/scholar/etc) |
|
| 209 |
+
| `/dataset/finetune_raw_nsfw/` | `nsfw` | 30 | 16 | 480 | suggestive (bath/lingerie/bedroom/etc) |
|
| 210 |
+
| `/dataset/finetune_raw_explicit/` | `explicit` | 30 | 16 | 480 | explicit sex acts #1-30 |
|
| 211 |
+
| `/dataset/finetune_raw_explicit_v2/` | `explicit` | 60 | 16 | 960 | explicit sex acts #31-90、consensual framing 8 件修正 |
|
| 212 |
+
| **合計** | | **300 themes** | | **~4800** | |
|
| 213 |
+
|
| 214 |
+
訓練時は AnimaImageCaptionDataset の rglob で全 subdir 自動取り込み (親 `/dataset/` を指定)。
|
| 215 |
+
|
| 216 |
+
### 5.8.2 ファインチューニング訓練計画
|
| 217 |
+
|
| 218 |
+
完成後 `train_finetune_4variants` で 4 hyperparam 並列実行 (各 B200):
|
| 219 |
+
|
| 220 |
+
| variant | rank | lr | epochs | drop_artist | 狙い |
|
| 221 |
+
|---|---|---|---|---|---|
|
| 222 |
+
| A baseline | 32 | 2e-5 | 3 | 0.0 | Anima 公式推奨そのまま |
|
| 223 |
+
| B higher capacity | 64 | 2e-5 | 3 | 0.0 | rank 倍で細部表現↑ |
|
| 224 |
+
| C safer LR | 32 | 1e-5 | 4 | 0.0 | 過学習回避 + step ↑ |
|
| 225 |
+
| D artist focus | 32 | 3e-5 | 3 | 0.0 | lr 強めで style 強化 |
|
| 226 |
+
|
| 227 |
+
**`llm_adapter_lr = 0` 必須** (Anima 公式既知識保護、本 repo `configs/phase1_anima.toml` 既設定)。
|
| 228 |
+
|
| 229 |
+
## 5.9 実走中の状態スナップショット (2026-05-17 時点)
|
| 230 |
+
|
| 231 |
+
> ⚠️ このセクションは **2026-05-17 時点のスナップショット**。実際の Modal volume 上の
|
| 232 |
+
> path / 中身は時間と共に変わる。最新は `modal volume ls anima-outputs` で確認。
|
| 233 |
+
> 現在配布済の LoRA は HF [darask0/anima-distill-loras](https://huggingface.co/darask0/anima-distill-loras) に集約。
|
| 234 |
+
|
| 235 |
+
このリポジトリで現在 Modal 上に保存されている / 訓練中の LoRA とその想定用途。
|
| 236 |
+
中断/再開時の参照用:
|
| 237 |
+
|
| 238 |
+
| LoRA | path on `anima-outputs` volume | source method | 状態 (当時) | コスト |
|
| 239 |
+
|---|---|---|---|---|
|
| 240 |
+
| **traj_final** | `/output/traj_full/traj_final.safetensors` | ① Z-Image trajectory imitation | ✅ 完走 (2000 step) | $18 |
|
| 241 |
+
| **traj_extended** | `/output/traj_extended_8000/traj_final.safetensors` | ① Z-Image 延長 (6000 step 追加) | 🟢 進行中 | ~$54 |
|
| 242 |
+
| **traj_rank128** | `/output/traj_rank128/traj_final.safetensors` | ① rank 32→128 cold-start | 🟢 進行中 | ~$50 |
|
| 243 |
+
| **dmd2_student_final** | `/output/dmd2_full/dmd2_student_final.safetensors` | ② DMD2 + TrigFlow | ✅ 完走 (3000 outer) | $19 |
|
| 244 |
+
| **ladd_v2_fast** | `/output/ladd_v2_fast/ladd_student_final.safetensors` | ③ LADD-v2 (bug 修正版) | 🟢 進行中 | ~$17 |
|
| 245 |
+
| **reflow_final** | `/output/reflow_full/reflow_final.safetensors` | ④ Reflow (rfpp) | 🟢 進行中 (~7000 step) | ~$8 |
|
| 246 |
+
| **pcm_final** | `/output/pcm_full/pcm_final.safetensors` | ⑤ PCM (SD3 流派) | 🟢 進行中 | ~$36 |
|
| 247 |
+
| **sid_student_final** | `/output/sid_full/sid_student_final.safetensors` | ⑥ SiD2 (data-free) | 🟢 進行中 | ~$18 |
|
| 248 |
+
| **shortcut_final** | `/output/shortcut_full/shortcut_final.safetensors` | ⑦ Shortcut Models (d-head) | 🟢 進行中 | ~$25 |
|
| 249 |
+
| **draftp_final** (Z-Image warm) | `/output/draftp_full/draftp_final.safetensors` | ⑧ DRaFT+ HPSv2 on ① | ✅ 完走 | $3 |
|
| 250 |
+
| **draftp_on_turbo** | `/output/draftp_on_turbo/draftp_final.safetensors` | ⑧ DRaFT+ HPSv2 on Civitai Turbo | ✅ 完走 | $3 |
|
| 251 |
+
|
| 252 |
+
中間 cache:
|
| 253 |
+
- `/dataset/teacher_x0_cache/` — 5000 サンプル、teacher 20-step CFG=4.5 の x0 + Qwen3 emb
|
| 254 |
+
- `/dataset/reflow_cache/` — 同上 + `noise` も保存 (Reflow / Shortcut 用)
|
| 255 |
+
- `/dataset/teacher_x0_smoke/` — 50 サンプル smoke 用
|
| 256 |
+
- `/models/loras/anima_turbo.safetensors` — Civitai 公式 (warm-start 用)
|
| 257 |
+
- `/models/hpsv2/HPS_v2_compressed.pt` — DRaFT+ 用 reward model
|
| 258 |
+
|
| 259 |
+
比較生成: `compare_distill_loras` (4 条件) と `compare_all_methods` (全 9-11 条件、final LoRA
|
| 260 |
+
存在を自動判定) の 2 種を `modal_app.py` に実装済。後者は 5 残り手法の本番完了後に走らせる。
|
| 261 |
+
|
| 262 |
+
実コスト累計 (2026-05-17 時点): **~$200** (smoke + 失敗 + 完走 + 進行中の使用済分込み)、
|
| 263 |
+
予算 $300 の 67%。
|
| 264 |
+
|
| 265 |
+
### 5.9.2 Modal volume 構成 (4 volumes)
|
| 266 |
+
|
| 267 |
+
本リポジトリは **4 つの Modal volume** を使い分けて状態を持つ。volume mount は
|
| 268 |
+
`modal_app.py::VOLUMES` で定義。
|
| 269 |
+
|
| 270 |
+
#### A. `anima-models` (~30 GB) — base モデル + 全 LoRA + reward weights
|
| 271 |
+
|
| 272 |
+
mount: `/models`、create_if_missing=True
|
| 273 |
+
|
| 274 |
+
```
|
| 275 |
+
checkpoints/
|
| 276 |
+
anima-base-v1.0.safetensors ← 現用 base (4.18 GB)
|
| 277 |
+
hakushiMixAnima_v02.safetensors ← (symlink, comfyui-anima-models から)
|
| 278 |
+
qwen_3_06b_base.safetensors ← text encoder
|
| 279 |
+
qwen_image_vae.safetensors ← VAE
|
| 280 |
+
phase_a_distilled.safetensors ← 旧 Phase A 失敗 ckpt (~$70 で失敗、残ってる)
|
| 281 |
+
phase_a_step1000.safetensors ← 旧 Phase A 中間
|
| 282 |
+
anima-preview3-base.safetensors ← ❌ 削除済 (v1.0 切替時)
|
| 283 |
+
|
| 284 |
+
loras/
|
| 285 |
+
anima_turbo.safetensors ← Civitai 公式 (warm-start のデファクト)
|
| 286 |
+
z_image_traj_final.safetensors ← ① Z-Image 蒸留 (preview3 起点、完走 LoRA)
|
| 287 |
+
dmd2_student_final.safetensors ← ② DMD2 (preview3 起点、完走 LoRA)
|
| 288 |
+
reflow_final.safetensors ← ④ Reflow (preview3 起点、完走 LoRA)
|
| 289 |
+
draftp_on_zimage.safetensors ← ⑩ DRaFT+ on Z-Image (品質追加学習)
|
| 290 |
+
draftp_on_turbo.safetensors ← B: DRaFT+ on Civitai Turbo (reward hacking 検証済、品質劣化)
|
| 291 |
+
ladd_v2_step500.safetensors ← ③ LADD v2 部分 (停止前 step 500)
|
| 292 |
+
pcm_step500.safetensors ← ⑤ PCM 部分 (停止前 step 500)
|
| 293 |
+
anima-highres-aesthetic-boost.safetensors ← (symlink, comfyui-anima-models から)
|
| 294 |
+
phase_b/d/f_4step_lora.safetensors ← 旧 R3GAN 試行の残骸 LoRA
|
| 295 |
+
|
| 296 |
+
hpsv2/
|
| 297 |
+
HPS_v2_compressed.pt ← DRaFT+ 用 reward model (1.97 GB)
|
| 298 |
+
|
| 299 |
+
torchhub/ ← torch.hub cache
|
| 300 |
+
cache/ ← misc cache (sageattention 等)
|
| 301 |
+
```
|
| 302 |
+
|
| 303 |
+
#### B. `anima-dataset` (~40 GB) — 全データセット + 比較生成出力
|
| 304 |
+
|
| 305 |
+
mount: `/dataset`、create_if_missing=True
|
| 306 |
+
|
| 307 |
+
**A. 元データ / self-distillation**:
|
| 308 |
+
- `raw/` — Anima base 自己生成 5000 (.png + .txt)、初期 distill 用
|
| 309 |
+
- `samples/`, `turbo_test/`, `compare/` — ��去の動作確認生成
|
| 310 |
+
|
| 311 |
+
**B. 蒸留用 precompute cache**:
|
| 312 |
+
- `teacher_x0_smoke/` — 50 サンプル smoke 用
|
| 313 |
+
- `teacher_x0_cache/` — 5000 サンプル × (x0 + emb)、LADD/PCM/SiD2 用
|
| 314 |
+
- `reflow_cache/` — 同上 + noise 保存、Reflow / Shortcut 用
|
| 315 |
+
|
| 316 |
+
**C. 比較 / 検証生成**:
|
| 317 |
+
- `compare_z_vs_dmd2/` — 80 PNG (base / turbo / ① / ② × 8/4 step)
|
| 318 |
+
- `verify_completed/` — 旧 verify (workflow bug で 0 images、失敗)
|
| 319 |
+
- `verify_v1_sage/` — v1.0 + sageattention で 13 条件 verify (1 prompt × 13)
|
| 320 |
+
- `ckpt_health_check/` — quick_check_ckpt 出力 (PCM step 500 等)
|
| 321 |
+
|
| 322 |
+
**D. ファインチューニング dataset (現在進行中、~4800 枚予定)**:
|
| 323 |
+
- `finetune_raw/` — SFW v1 (60 themes × 16 seeds = 960)
|
| 324 |
+
- `finetune_raw_v2/` — SFW v2 (animal/mytho/sports 60×16 = 960)
|
| 325 |
+
- `finetune_raw_v3/` — SFW v3 (職業/世界観 60×16 = 960)
|
| 326 |
+
- `finetune_raw_nsfw/` — NSFW suggestive (30×16 = 480)
|
| 327 |
+
- `finetune_raw_explicit/` — EXPLICIT v1 (30×16 = 480)
|
| 328 |
+
- `finetune_raw_explicit_v2/` — EXPLICIT v2 #31-90 (60×16 = 960)
|
| 329 |
+
|
| 330 |
+
#### C. `anima-outputs` (~30 GB) — 訓練 LoRA / ckpt の保存先
|
| 331 |
+
|
| 332 |
+
mount: `/output`、create_if_missing=True
|
| 333 |
+
|
| 334 |
+
**完走 (preview3 起点)**:
|
| 335 |
+
- `traj_full/` — ① Z-Image final ($18)
|
| 336 |
+
- `dmd2_full/` — ② DMD2 final ($19)
|
| 337 |
+
- `reflow_full/` — ④ Reflow final ($13)
|
| 338 |
+
- `draftp_full/` — ⑩ DRaFT+ on Z final ($3)
|
| 339 |
+
- `draftp_on_turbo/` — B: DRaFT+ on Turbo final ($3)
|
| 340 |
+
|
| 341 |
+
**中断 / 部分 (preview3、~$80-100 浪費)**:
|
| 342 |
+
- `ladd_v2_fast/` — ③ LADD v2 step 500 ckpt まで
|
| 343 |
+
- `pcm_full/` — ⑤ PCM step 500 ckpt まで
|
| 344 |
+
- `sid_full/` — ⑦ SiD2 outer ~4200 まで (停止時)
|
| 345 |
+
- `traj_extended_8000/` — A: Z-Image extend (停止)
|
| 346 |
+
- `traj_rank128/` — E: rank 128 (停止)
|
| 347 |
+
- `shortcut_full/` — ⑦ Shortcut 異常遅延 step 500 まで ($63 損)
|
| 348 |
+
|
| 349 |
+
**旧 R3GAN 試行 (5 連続失敗の残骸)**:
|
| 350 |
+
- `distill/`, `distill_e/`, `distill_f/`, `distill_f_full/` — Phase A/B/C 試行
|
| 351 |
+
|
| 352 |
+
**smoke 各種**:
|
| 353 |
+
- `*_smoke/` — 各手法の smoke test 出力 (1-5 step、debug 用)
|
| 354 |
+
|
| 355 |
+
#### D. `comfyui-anima-models` (~5 GB) — user アップロード外部モデル
|
| 356 |
+
|
| 357 |
+
mount: `/comfyui_anima_models`、create_if_missing=**False** (user が事前アップロード)
|
| 358 |
+
|
| 359 |
+
```
|
| 360 |
+
diffusion_models/
|
| 361 |
+
hakushiMixAnima_v02.safetensors ← ファインチューニング用 base
|
| 362 |
+
loras/
|
| 363 |
+
anima-highres-aesthetic-boost.safetensors ← ファインチューニング用 quality boost LoRA
|
| 364 |
+
text_encoders/, vae/ ← 空 (将来用 placeholder)
|
| 365 |
+
```
|
| 366 |
+
|
| 367 |
+
ファインチューニング用 dataset 生成時に `/models/{checkpoints,loras}` へ symlink される
|
| 368 |
+
(`generate_finetune_chunk` 内で自動)。
|
| 369 |
+
|
| 370 |
+
#### 累計サイズ + コスト
|
| 371 |
+
|
| 372 |
+
| volume | 概算サイズ | 月額保管料 (Modal $0.15/GB/月) |
|
| 373 |
+
|---|---|---|
|
| 374 |
+
| anima-models | ~30 GB | ~$4.5 |
|
| 375 |
+
| anima-dataset | ~40 GB | ~$6 |
|
| 376 |
+
| anima-outputs | ~30 GB | ~$4.5 |
|
| 377 |
+
| comfyui-anima-models | ~5 GB | ~$0.75 |
|
| 378 |
+
| **合計** | **~105 GB** | **~$16/月** |
|
| 379 |
+
|
| 380 |
+
不要 dir は `cleanup_checkpoints` Modal function 等で適時間引きが推奨 (古い phase_a/b/c 系、
|
| 381 |
+
shortcut_full、放置 smoke 各種、要らない finetune subdir は数十 GB 占有してる)。
|
| 382 |
+
|
| 383 |
+
### 5.9.1 Anima 公式推奨 prompt / 推論設定 / ファインチューニング設定
|
| 384 |
+
|
| 385 |
+
[CircleStone Labs/Anima HF README](https://huggingface.co/circlestone-labs/Anima) から
|
| 386 |
+
抜粋した推奨設定。本リポジトリの全 workflow / generate_dataset / fine-tune script は
|
| 387 |
+
この推奨に準拠している。
|
| 388 |
+
|
| 389 |
+
**Prompt format**:
|
| 390 |
+
|
| 391 |
+
```
|
| 392 |
+
[quality/meta/year/safety] [1girl/1boy/etc] [character] [series] [@artist] [general]
|
| 393 |
+
```
|
| 394 |
+
|
| 395 |
+
- Quality prefix (positive 先頭): `masterpiece, best quality, score_7, safe`
|
| 396 |
+
- Negative: `worst quality, low quality, score_1, score_2, score_3, artist name`
|
| 397 |
+
- tag は **lowercase + space 区切り** (underscore は score タグだけ: `score_7`)
|
| 398 |
+
- Danbooru タグより **Gelbooru タグを優先**
|
| 399 |
+
- artist tag は **`@` プレフィックス必須** (なしだと効果激減)、`[artist]` 位置に置く
|
| 400 |
+
- 多語 artist は半角 space (例: `@nnn yryr`)
|
| 401 |
+
- 別解釈 disambiguator がある場合は paren escape (例: `@artist_name \(circle_name\)`)
|
| 402 |
+
- 複数 artist を `,` 区切りで列挙可
|
| 403 |
+
|
| 404 |
+
**Recommended sampler / scheduler / CFG / steps**:
|
| 405 |
+
|
| 406 |
+
| 推奨 | 値 / 説明 |
|
| 407 |
+
|---|---|
|
| 408 |
+
| sampler | **`er_sde`** (中立、フラット彩色、シャープライン)、`euler_a` (柔らかめ、2.5D 傾向)、`dpmpp_2m_sde_gpu` (バラエティ豊か、暴走しがち) |
|
| 409 |
+
| scheduler | **`simple`** (デフォルト)、`beta57` (RES4LYF custom node、リアル系・絵画調用) |
|
| 410 |
+
| sigma_shift | **`3.0`** (`ModelSamplingAuraFlow` で必須、本リポジトリ全 workflow で設定済) |
|
| 411 |
+
| CFG | **4-5** |
|
| 412 |
+
| step | **30-50** |
|
| 413 |
+
| resolution | **512² から 1536²** までサポート (1024² 中心が無難) |
|
| 414 |
+
| VAE | `qwen_image_vae.safetensors` |
|
| 415 |
+
| Text encoder | `qwen_3_06b_base.safetensors` |
|
| 416 |
+
| Diffusion model | `anima-base-v1.0.safetensors` |
|
| 417 |
+
|
| 418 |
+
**Fine-tuning best practices** (Anima ��式抜粋):
|
| 419 |
+
|
| 420 |
+
- **`llm_adapter_lr = 0` 必須** — adapter は影響力が強く既知識を多く持つため訓練すると壊れる
|
| 421 |
+
(本 repo `configs/phase1_anima.toml` で設定済)
|
| 422 |
+
- 低 LR start: rank 32 LoRA で **`lr = 2e-5`** から
|
| 423 |
+
- light touch training — モデルが既に多様な visual concept を持っているので無理に押し込まない
|
| 424 |
+
- captioning は **Danbooru tag + 自然言語の併用** OK
|
| 425 |
+
- 例: 公式 [style LoRA on Civitai](https://civitai.com/models/2536147) (dataset + config 公開)
|
| 426 |
+
|
| 427 |
+
**Content policy / restrictions**:
|
| 428 |
+
|
| 429 |
+
- safety tag: `safe / sensitive / nsfw / explicit` を positive / negative 両方で使い分ける
|
| 430 |
+
- text rendering は弱い (長文を画像に書かせない)
|
| 431 |
+
- 複数キャラ: character 名 → 基本属性の順で書くと混同回避
|
| 432 |
+
- prompt weighting は SDXL より強めに必要 (例: `(chibi:2)`)
|
| 433 |
+
- 自然言語 prompt は **最低 2 文以上** が安定 (極端に短いと予測不能)
|
| 434 |
+
- **非商用ライセンス縛り**、商用問い合わせ `tdrussell@circlestone.ai`
|
| 435 |
+
|
| 436 |
+
## 5.10 品質評価の方法論 (reward hacking 実証あり)
|
| 437 |
+
|
| 438 |
+
**「ディテール多い・彩度高い」≠「良い」**。蒸留 LoRA や reward fine-tuning の出力を評価
|
| 439 |
+
する際は、必ず以下の軸で分解して判断する:
|
| 440 |
+
|
| 441 |
+
| 評価軸 | 確認内容 |
|
| 442 |
+
|---|---|
|
| 443 |
+
| (a) **target style 維持** | anime 系を期待しているなら anime のままか? 西洋イラスト / リアル系にドリフトしていないか? |
|
| 444 |
+
| (b) **キャラクター固有要素の精度** | 例: Touhou Flandre Scarlet なら金髪+赤目+クリスタル翼、Remilia なら青髪+赤目+コウモリ翼 が正確か? |
|
| 445 |
+
| (c) **prompt adherence** | 指示したポーズ (例: hands raised pointing toward viewer with open palms)、構図、背景要素を反映しているか? |
|
| 446 |
+
| (d) **破綻ポイント** | 手指の本数・関節・方向、複数キャラの顔混じり、関節破綻、bad anatomy |
|
| 447 |
+
| (e) **生成時間** | per-image gen_time (`_summary.json` から)、step 数 × sampler × LoRA 構成で比較 |
|
| 448 |
+
|
| 449 |
+
**DRaFT+ HPSv2 reward fine-tuning の reward hacking 実証 (2026-05-17)**:
|
| 450 |
+
|
| 451 |
+
- 仮説: 公式 Anima Turbo に HPSv2 reward fine-tuning (DRaFT+ K=1 LV、kl_coeff=0.2、1500 step、
|
| 452 |
+
$3) を後付けすれば品質が公式超えできる?
|
| 453 |
+
- 結果: HPSv2 score は **25.78 → 38.99 (+13.21)** と大幅向上 (loss 順調、KL 0.2-0.3 安定)
|
| 454 |
+
- **しかし視覚的には劣化** (ユーザー評価): 西洋イラスト風にドリフト、Flandre/Remilia の固有
|
| 455 |
+
翼が曖昧化、両キャラの衣装がほぼ同じに、prompt の手のポーズが崩れる、装飾過多 (シャンデリア
|
| 456 |
+
+ ランプ + 飾り) でゴチャゴチャ
|
| 457 |
+
- 原因: HPSv2 は「人間が好きそうな generic な見栄え」preference を学習しており、Touhou anime
|
| 458 |
+
style や character accuracy を直接最適化していない。reward が指す方向と用途が乖離した
|
| 459 |
+
|
| 460 |
+
教訓:
|
| 461 |
+
- **reward score 上昇 ≠ 品質向上**。reward 系は必ず視覚検証
|
| 462 |
+
- HPSv2 / PickScore / ImageReward 等は「平均的に綺麗」を上げるだけ、特定 niche style には不向き
|
| 463 |
+
- 蒸留 LoRA の評価では、必ず **「複数の評価軸を分解」して判定**。1 軸だけ (例: ディテール量)
|
| 464 |
+
で判断しない
|
| 465 |
+
- 「score が上がった」「ディテールが増えた」を理由に勝者宣言しない、視覚的に必ず確認する
|
| 466 |
+
|
| 467 |
+
**verify 用 prompt の設計指針** ([scripts/verify_prompts.txt](../scripts/verify_prompts.txt)):
|
| 468 |
+
|
| 469 |
+
単純な single-character prompt では LoRA の差が出ない。**「壊れにくいもの」より「壊れやすい
|
| 470 |
+
もの」をテストする**:
|
| 471 |
+
|
| 472 |
+
- 複数キャラクター (face mix / 属性入れ替えが起きやすい)
|
| 473 |
+
- キャラ毎に異なる属性 (髪色、目色、翼種別)
|
| 474 |
+
- 手・指の特定ポーズ (open palms、pointing)
|
| 475 |
+
- 詳細背景 + foreground キャラ両立
|
| 476 |
+
- target style の制約 (anime style 維持を強制)
|
| 477 |
+
|
| 478 |
+
これで初めて LoRA 間の優劣が顕在化する。
|
| 479 |
+
|
| 480 |
+
## 5.11 次セッション引き継ぎ (2026-05-17 23:xx 中断時点)
|
| 481 |
+
|
| 482 |
+
> ⚠️ 2026-05-17 時点のスナップショット。**現在は v1.0 base で PCM が cold-start 完走済**
|
| 483 |
+
> ([darask0/anima-distill-loras/pcm](https://huggingface.co/darask0/anima-distill-loras/tree/main/pcm))。
|
| 484 |
+
> 本セクションは当時の状態を歴史記録として保持。
|
| 485 |
+
|
| 486 |
+
### 1. 完走済 (Anima preview3 base 起点、すべて使用可)
|
| 487 |
+
|
| 488 |
+
| LoRA | 出力 path | コスト |
|
| 489 |
+
|---|---|---|
|
| 490 |
+
| ① Z-Image traj imitation | `/output/traj_full/traj_final.safetensors` | $18 |
|
| 491 |
+
| ② DMD2 + TrigFlow | `/output/dmd2_full/dmd2_student_final.safetensors` | $19 |
|
| 492 |
+
| ④ Reflow rfpp | `/output/reflow_full/reflow_final.safetensors` | $13 |
|
| 493 |
+
| ⑩ DRaFT+ on ① Z-Image | `/output/draftp_full/draftp_final.safetensors` | $3 |
|
| 494 |
+
| B: DRaFT+ on Civitai Turbo | `/output/draftp_on_turbo/draftp_final.safetensors` | $3 ⚠️ reward hacking 検証で **品質劣化** 確認 |
|
| 495 |
+
|
| 496 |
+
### 2. 中断 (preview3 起点、user が「古いから止めて」と判断し全停止 2026-05-17)
|
| 497 |
+
|
| 498 |
+
| 手法 | 部分 ckpt | 中断時 step | 浪費コスト |
|
| 499 |
+
|---|---|---|---|
|
| 500 |
+
| ③ LADD v2 | `/output/ladd_v2_fast/ladd_student_step00500.safetensors` | ~500/5000 | ~$5 |
|
| 501 |
+
| ⑤ PCM | `/output/pcm_full/pcm_step00500.safetensors` | ~760/5000 | ~$30 |
|
| 502 |
+
| ⑦ SiD2 | `/output/sid_full/` (sample_every=500 で step ~4000 までの ckpt) | ~4200/5000 | ~$15 |
|
| 503 |
+
| ⑧ Shortcut | `/output/shortcut_full/shortcut_step00500.safetensors` | ~560/4000 (64s/step 異常遅延で abort) | $63 |
|
| 504 |
+
| A: Z-Image extend | `/output/traj_extended_8000/` (step ~1360 までの ckpt) | ~1360/6000 | ~$13 |
|
| 505 |
+
| E: rank 128 | `/output/traj_rank128/` (step ~1440 までの ckpt) | ~1440/2000 | ~$13 |
|
| 506 |
+
|
| 507 |
+
中断合計浪費: **~$139**。**ファイルは残してある**、user 判断で再開 or 削除。
|
| 508 |
+
|
| 509 |
+
### 3. ファインチューニング dataset (新規、hakushi base、ほぼ完成)
|
| 510 |
+
|
| 511 |
+
| subdir | safety | 完了状況 |
|
| 512 |
+
|---|---|---|
|
| 513 |
+
| `/dataset/finetune_raw/` | safe | ✅ 960 |
|
| 514 |
+
| `/dataset/finetune_raw_v2/` | safe | ✅ 960 |
|
| 515 |
+
| `/dataset/finetune_raw_v3/` | safe | ✅ 960 |
|
| 516 |
+
| `/dataset/finetune_raw_nsfw/` | nsfw | ✅ 480 |
|
| 517 |
+
| `/dataset/finetune_raw_explicit/` | explicit | ✅ 480 |
|
| 518 |
+
| `/dataset/finetune_raw_explicit_v2/` | explicit | ✅ 960 |
|
| 519 |
+
| **合計** | | ✅ **4800 / 4800 完成** |
|
| 520 |
+
|
| 521 |
+
訓練 prompt: `scripts/finetune_prompts*.txt`、artist tag は **秘密 ♥** (§5.8.1 参照)。
|
| 522 |
+
|
| 523 |
+
### 4. 次セッションで user 判断待ちのこと (最重要)
|
| 524 |
+
|
| 525 |
+
ファインチューニング dataset 完成後、**どちらに進むか user 判断**:
|
| 526 |
+
|
| 527 |
+
**Path A (推奨): ファインチューニング集中**
|
| 528 |
+
- `train_finetune_4variants` で 4 hyperparam 並列 (A: rank 32 lr 2e-5 / B: rank 64 lr 2e-5 / C: rank 32 lr 1e-5 epochs 4 / D: rank 32 lr 3e-5)
|
| 529 |
+
- 各 ~3-4h、計 ~$120
|
| 530 |
+
- 蒸留は公式 Anima Turbo に任せる (compare_z_vs_dmd2 で Turbo 圧勝が実証済)
|
| 531 |
+
- 自前 fine-tune LoRA × 公式 Turbo の **スタック** が現実解
|
| 532 |
+
|
| 533 |
+
**Path B: v1.0 base で蒸留 LoRA を再訓練**
|
| 534 |
+
- 既存の `/dataset/raw/` (preview3 self-distill) は base 違いで不適
|
| 535 |
+
- v1.0 base で self-distillation dataset 新規生成必要 (~$25)
|
| 536 |
+
- その後 5 蒸留手法を v1.0 で再訓練 (~$80-120)
|
| 537 |
+
- 計 ~$120-145
|
| 538 |
+
- 公式 Turbo に勝てる見込み低 (前回比較で実証)
|
| 539 |
+
|
| 540 |
+
**Path C: 両方やる**
|
| 541 |
+
- Path A 先行で完成見て、余力あれば Path B
|
| 542 |
+
- 計 ~$250-280
|
| 543 |
+
|
| 544 |
+
> **2026-05-18 update**: PCM を Path B 流に v1.0 base で cold-start 完走、
|
| 545 |
+
> HF 配布まで完了 ([詳細は HF model card](https://huggingface.co/darask0/anima-distill-loras/tree/main/pcm))。
|
| 546 |
+
|
| 547 |
+
### 5. 次 Claude が必ず守るルール
|
| 548 |
+
|
| 549 |
+
- **Artist tag は秘密** (§5.8.1)。Claude が prompt 生成で必要な場合、user に直接質問する。
|
| 550 |
+
推測 / 履歴引用しない。`scripts/finetune_prompts*.txt` には実 tag が入ってるが README / chat
|
| 551 |
+
出力では具体名を引用しない
|
| 552 |
+
- **Modal CLI on Windows 必須 prefix**: `MSYS_NO_PATHCONV=1 PYTHONIOENCODING=utf-8 PYTHONUTF8=1`
|
| 553 |
+
- **画像品質評価**: 「ディテール多い ≠ 良い」、必ず user judgment を仰ぐ (§5.10 reward hacking 実証)
|
| 554 |
+
- **workflow JSON node ID**: positive=5, latent=7, KSampler=8 (`generate_dataset.py::patch_workflow` が hardcode)
|
| 555 |
+
- **base 切替の影響**: preview3 LoRA を v1.0 で使うと style drift する (§5.7.1)
|
| 556 |
+
- **rank 違いの LoRA は warm-start 不可** (`load_warm_lora` で 0 keys matched エラー)、cold-start 必要
|
| 557 |
+
- **LADD discriminator bug fix**: `gradient_to_input` フラグ追加済、G phase は True、D phase は False
|
| 558 |
+
- **Modal function param は lowercase**: 大文字 `K: int` は Modal CLI で `k=1` に変換され抹消、`k_grad` などに
|
| 559 |
+
- **sageattention 有効**: `_base_image` に同梱、`generate_dataset.py` で auto-enable
|
| 560 |
+
|
| 561 |
+
### 6. 現コスト累計 + 予算
|
| 562 |
+
|
| 563 |
+
- 完走 LoRA + smoke + 失敗 retry + dataset: **~$200**
|
| 564 |
+
- 中断蒸留訓練の浪費: **~$139**
|
| 565 |
+
- 当初予算 $300 → **~$339 で +$39 超過**
|
| 566 |
+
- 推定 ファインチューニング 4 並列: +$120 → 完成時総額 ~$460
|
| 567 |
+
- user が「金は OK」と承認 (実コストは全部 Modal dashboard で監視可能)
|
| 568 |
+
|
| 569 |
+
### 7. 中断時 Modal で active な task
|
| 570 |
+
|
| 571 |
+
セッション終了時に bash background は全部止めてよい (Modal cloud は無影響)。
|
| 572 |
+
ただし Modal app は **app_stop コマンドで明示停止が必要** (止めないと課金続行):
|
| 573 |
+
|
| 574 |
+
```bash
|
| 575 |
+
modal app list # 全 app 確認
|
| 576 |
+
modal app stop <ap-xxxxx> --yes # 個別停止
|
| 577 |
+
```
|
| 578 |
+
|
| 579 |
+
次セッション開始時は `modal app list` で再確認して、無駄に残った app が無いか確認推奨。
|
| 580 |
+
本セッション中断時には:
|
| 581 |
+
- 6 訓練 (LADD v2 / PCM / SiD2 / A / E / Shortcut) はすべて停止済
|
| 582 |
+
- dataset 生成: ✅ **全 6 subdir 4800 枚完成** (SFW v3 も 2026-05-17 終わり際で完了)
|
| 583 |
+
- 残 active task なし、Modal 課金は volume 保管料 ($16/月) のみ
|
docs/operations.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 実装 / 運用 Tips + トラブルシューティング
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md)
|
| 4 |
+
|
| 5 |
+
[distillation.md §4.7](distillation.md#47-周辺で踏んだ-modal--diffusion-pipe--comfyui-罠) の diffusion-pipe / ComfyUI 罠に加え、
|
| 6 |
+
2026-05 試行 (5 手法移植) で踏んだ追加の罠と対処、よくあるトラブルの解決策。
|
| 7 |
+
|
| 8 |
+
## Modal CLI on Windows + JP (cp932) locale
|
| 9 |
+
|
| 10 |
+
毎回必須:
|
| 11 |
+
```bash
|
| 12 |
+
MSYS_NO_PATHCONV=1 PYTHONIOENCODING=utf-8 PYTHONUTF8=1 modal run ...
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
| 罠 | 症状 | 対処 |
|
| 16 |
+
|---|---|---|
|
| 17 |
+
| cp932 codec error | Modal CLI 出力の Unicode ✓ や ● を encode できず即死 | `PYTHONIOENCODING=utf-8 PYTHONUTF8=1` |
|
| 18 |
+
| Git Bash MSYS path mangling | `--dataset-path /dataset/raw` が `C:/Program Files/Git/dataset/raw` に化ける | `MSYS_NO_PATHCONV=1` |
|
| 19 |
+
| Modal token が silent revoke | `.modal.toml` は valid、`modal config show` で見える、しかし API 呼び出しで "Token not found" | `MODAL_LOGLEVEL=DEBUG modal app list` でサーバー側のエラーを確認 → ユーザーが `modal token new` で再発行 |
|
| 20 |
+
| `modal app stop` が対話確認で停止 | `Are you sure...? [y/N]:` で固まる | `--yes` を必ず付ける |
|
| 21 |
+
|
| 22 |
+
## diffusion-pipe + Anima 移植時の罠
|
| 23 |
+
|
| 24 |
+
| 罠 | 症状 | 対処 |
|
| 25 |
+
|---|---|---|
|
| 26 |
+
| `_find_blocks` ヘルパーが `llm_adapter.blocks` を選ぶ | Anima DiT は本体 28 blocks + llm_adapter 6 blocks。名前 heuristic で adapter 側を拾うと **5 head すべて学習されない** | `transformer.blocks` を direct 参照、または `"llm_adapter" not in name` で filter (`anima_ladd_disc.py::_find_blocks` 参照) |
|
| 27 |
+
| LPIPS reg が OOM | `compute_regularization` 内で **student rollout を grad-through で 2 回目** 走らせると 16 forward が graph に乗り 178 GiB 食い尽くす | (1) `--lpips-weight 0` で disable (2) `--lpips-every N` で間欠的に有効化 (Reflow パターン) (3) 解像度 1024 → 768 に下げる |
|
| 28 |
+
| PEFT LoRA 2 adapter 切替 | DMD2 で同じ base に student + fake_score を attach する場合、**毎 forward で `set_adapter(name)` を呼ばないと 片方の重みが leak する** | `make_velocity_fn(peft_model, adapter_name)` のような callable wrapper で隠蔽 (`train_dmd2_official.py` 参照) |
|
| 29 |
+
| Civitai Turbo LoRA → PEFT 形式変換 | warm-start 時、ComfyUI 形式 `diffusion_model.<…>.lora_A.weight` を PEFT 形式 `base_model.model.<…>.lora_A.default.weight` に変換必要 | `train_traj.py::convert_comfy_to_peft_lora` (`_convert_peft_to_comfy_lora` の逆向き) |
|
| 30 |
+
| warm-start key match | wide LoRA target が `x_embedder / t_embedder` を含まないため、Anima Turbo の 1016 keys のうち **968 keys のみ match** (12 keys skip、48 keys missing) | 致命的ではない、warm-start として十分機能 |
|
| 31 |
+
| subprocess 内 `import modal` 失敗 | 訓練 script が `modal.Volume.from_name(...).commit()` を呼ぶと `ModuleNotFoundError: No module named 'modal'` (subprocess の Python path に Modal がない) | `try/except` で囲んで warning 出力のみ。Modal は subprocess 終了時に自動で volume commit する |
|
| 32 |
+
| **LADD discriminator の `torch.no_grad()` + `feat.detach()` で adv 勾配が student に届かない** | G phase で adv loss を `.backward()` しても勾配が disc の heads までで止まる。実質「Smooth L1 recon distill only」になり LADD のメリット消失 | `AnimaLADDDiscriminator.forward(...)` に `gradient_to_input` フラグを足す。G phase は `True` (teacher は frozen なので weight 更新されないが activations が残り x_input → student に勾配が流れる)、D phase は `False` (heads だけ訓練、メモリ節約) |
|
| 33 |
+
| **LADD bs=4 × accum=4 × misaligned_pairs_d で 170 s/step (smoke の 200x 遅い)** | bs=4 で D forward の cost が線形増、accum=4 で 4 倍 micro-step、misaligned で D batch を 2 倍に → 20+ full-DiT forward/step、B200 上で 170 s | **bs=2 accum=1 + misaligned 無効** に下げる → ~2 s/step (85x 高速化)。LADD は GAN なので大 batch 不要 (small noise の方が D の安定性に貢献)。`train_ladd.py` 引数: `--batch-size 2 --grad-accum 1` (`--misaligned-pairs-d` フラグを付けない) |
|
| 34 |
+
| **異なる rank の LoRA を warm-start に使えない** | `--lora-rank 128` で `--warm-lora /models/loras/anima_turbo.safetensors` (rank 32) を指定 → `load_warm_lora` 内で shape mismatch、`No LoRA keys matched` で abort | rank 変更時は **cold-start** で訓練 (warm-start を省略)。または rank padding helper を書く |
|
| 35 |
+
| **modal CLI の uppercase arg は失敗** | function param `K: int` を CLI から `--K 1` で渡しても Modal が `k=1` に lowercase 変換、`unexpected keyword argument 'k'` で abort | function param は **必ず lowercase** にする (例 `k_grad: int`)、CLI 側 `--k-grad 1` を渡す |
|
| 36 |
+
| **ComfyUI workflow JSON の `_comment` キーが node 扱いされて crash** | workflow JSON を iterate して `node.get("class_type")` する処理で、`_comment` (string value) が来ると `'str' object has no attribute 'get'` | `for node_id, node in wf.items(): if not isinstance(node, dict): continue` で skip |
|
| 37 |
+
| **workflow JSON の node ID が `patch_workflow` の hardcode と一致しないと全 generation が HTTP 400 で fail** | `scripts/generate_dataset.py::patch_workflow` は positive prompt を node "5"、latent を "7"、KSampler を "8" に hardcode。ユーザー由来 workflow の node ID が違うと `$PROMPT/$SEED` プレースホルダが置換されず ComfyUI が prompt graph reject | 新 workflow を作る時は **必ず node ID を 5=positive, 7=latent, 8=KSampler** で構築。`anima_workflow.json` 系を雛形にする |
|
| 38 |
+
| **reward fine-tuning (DRaFT+) で reward score 上昇 ≠ 視覚品質向上** | HPSv2 score +13.21 上昇したが視覚的には reward hacking が起き、anime → 西洋イラスト ドリフト + キャラ固有要素曖昧化 + prompt adherence 落下 | 必ず視覚検証する、score だけで「勝った」と判断しない。kl_coeff 上げる (0.2 → 0.5) や reward 種類変更 (HPSv2 → ImageReward) で軽減可能性、ただし根本解決ではない ([migration_log §5.10](migration_log.md#510-品質評価の方法論-reward-hacking-実証あり) 参照) |
|
| 39 |
+
| **base model 切替で既存 LoRA の出力 style が drift する** | preview3 で訓練した LoRA を v1.0 base で使うと chibi/SD 寄りに style が変わる (LoRA は動作する、出力分布が違う) | LoRA は train 時の base と一致させて使うのが筋。base 切替時は LoRA を再訓練するか style drift を許容する |
|
| 40 |
+
| **sageattention は .so 不要 (Triton JIT)** | `cache_sageattention_to_volume` で `/usr/local/lib/python3.11/site-packages/sageattention/` を volume copy しても `.so` が 0 個。一見「コンパイル失敗」に見える | 仕様。sageattention は Triton で runtime JIT、import 時に kernel が compile される。動作確認は CUDA kernel smoke test (`sageattn(q,k,v)` を呼ぶ) で行う |
|
| 41 |
+
| **ComfyUI で sageattention を効かせる** | デフォルトは torch SDPA、sageattention 入っていても自動使用されない | ComfyUI 起動引数に `--use-sage-attention` を追加。`generate_dataset.py` は `import sageattention` 成功時に自動で付与するよう改修済 |
|
| 42 |
+
| **Modal volume get で Japanese 文字パスに download すると後から Bash で見つけられない** | `modal volume get anima-dataset path/ "/c/Users/micro/Downloads/新しいフォルダー/dest/"` で download は成功するが、後続の `cd` / `find` / Python `os.walk` で日本語パスが broken or 空に見える | 一次 download 先は ASCII path に (例: `./verify_v1_local/`)、必要なら後で copy で日本語フォルダに移す |
|
| 43 |
+
| **Modal volume `rm` は subprocess 内では使えない** | training script が訓練中に preview3 を消そうとしてもファイルハンドルが mmap で開いている (PyTorch safetensors loader) | volume cleanup は別の Modal function で実行、training 中は触らない (実害は少ないが orchestration 注意) |
|
| 44 |
+
|
| 45 |
+
## コスト / 時間の実測値 (本リポジトリ 5 手法移植時)
|
| 46 |
+
|
| 47 |
+
すべて B200 ($6.25/h Modal)、batch=1-4、768-1024 解像度:
|
| 48 |
+
|
| 49 |
+
| 操作 | 時間 | コスト |
|
| 50 |
+
|---|---|---|
|
| 51 |
+
| Image rebuild (lpips 追加分) | ~5 分 (キャッシュ後 0 分) | ~$0.5 |
|
| 52 |
+
| B200 cold start | ~3 分 | ~$0.30 |
|
| 53 |
+
| Smoke test (1 step) | ~5-10 分 | ~$0.5-2 |
|
| 54 |
+
| Z-Image train 2000 step | ~3 h (5.2s/step) | **~$18** (見積もり $42 の半額) |
|
| 55 |
+
| DMD2 train 3000 outer (5 critic + 1 gen) | ~5.8 h (3.6s/step) | **~$19** (見積もり $36 の半額) |
|
| 56 |
+
| precompute_teacher_x0 5000 サンプル | ~1.5 h (0.69/s) | ~$11 |
|
| 57 |
+
| LADD train 5000 step bs=4 accum=4 | ~4.8 h (0.86s/step) | ~$30 |
|
| 58 |
+
| Reflow train 8000 step | ~3-4 h | ~$25 |
|
| 59 |
+
| PCM train 5000 step (1 grad + 3 no_grad) | ~6.5 h (4.7s/step) | ~$41 |
|
| 60 |
+
| **5 手法 + precompute × 2 トータル** | wall-clock ~8 h (並列) | **~$170** |
|
| 61 |
+
|
| 62 |
+
実測 step 時間は **predicted の概ね 半分** — Modal の B200 は H100 比 2-3x 速いことを織り込まないと過大見積もりになる。
|
| 63 |
+
|
| 64 |
+
**推論側 (verify 段階) の実測 (v1.0 base + sageattention 有効、B200、1024×1024)**:
|
| 65 |
+
|
| 66 |
+
| 条件 | per-image | 内訳 |
|
| 67 |
+
|---|---|---|
|
| 68 |
+
| LoRA 4-step CFG=1 | **3.5-4.0s** | sage 効果あり、ほぼ全 LoRA 同等 (Z-Image / DMD2 / Turbo / Reflow / DRaFT+ 系) |
|
| 69 |
+
| LoRA 8-step CFG=1 | **4.0-4.7s** | sage 効果あり、step 数比例 |
|
| 70 |
+
| base 30-step CFG=4.5 (LoRA strength=0) | **10.7s** | 品質基準ライン |
|
| 71 |
+
|
| 72 |
+
→ 蒸留 LoRA 4-step は base 30-step の **3 倍速 = $0.006/枚 vs $0.018/枚** (B200 $6.25/h 換算)。
|
| 73 |
+
|
| 74 |
+
## トラブルシューティング
|
| 75 |
+
|
| 76 |
+
### OOM (CUDA out of memory)
|
| 77 |
+
- `phase1_anima.toml` の `activation_checkpointing = true` を確認
|
| 78 |
+
- `[adapter].rank` を 32 まで下げる
|
| 79 |
+
- 解像度を 768 に落とす(`phase1_dataset.toml` の `resolutions`)
|
| 80 |
+
- それでもダメなら H100 80GB に上げる
|
| 81 |
+
|
| 82 |
+
### 学習が進まない
|
| 83 |
+
- データセット数枚で `epochs = 1` 試走 → エラーログを見る
|
| 84 |
+
- `modal app logs rapid-anima` でリアルタイム監視
|
| 85 |
+
- `llm_adapter_lr = 0` が効いているか(消えていると壊れる)
|
| 86 |
+
|
| 87 |
+
### 出力の絵が地味
|
| 88 |
+
- これは正常。Anima base は素なので、データセットを `score≥200` 等で
|
| 89 |
+
絞って色彩豊かな絵を集めると改善
|
| 90 |
+
- artist タグを残して `@favorite_artist1, @favorite_artist2` を
|
| 91 |
+
caption に入れたままにすると、それらの平均が出やすくなる
|
| 92 |
+
|
| 93 |
+
### diffusion-pipe が image build で失敗
|
| 94 |
+
- `requirements.txt` 内のバージョン競合が原因のことが多い
|
| 95 |
+
- `modal_app.py` の `.pip_install` 部分でバージョン固定済み
|
| 96 |
+
- それでも詰まる時は GitHub Issues 参照: https://github.com/tdrussell/diffusion-pipe/issues
|
| 97 |
+
|
| 98 |
+
### attention をもっと速くしたい
|
| 99 |
+
デフォルトは torch SDPA(flash-attention 2 が built-in で使われる)。
|
| 100 |
+
さらに 1.2-1.5x 速くしたい場合は、`modal_app.py` の image 定義の最後に追加:
|
| 101 |
+
|
| 102 |
+
```python
|
| 103 |
+
.run_commands(
|
| 104 |
+
# diffusion-pipe install 後にビルドして torch ABI を合わせる
|
| 105 |
+
"pip install --no-build-isolation sageattention",
|
| 106 |
+
# または flash-attn (source build, 30-40分):
|
| 107 |
+
# "pip install --no-build-isolation flash-attn",
|
| 108 |
+
)
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
依存解決の都合で順序が重要(diffusion-pipe より後ろ)。
|
| 112 |
+
|
| 113 |
+
### `modal volume put` で大きいデータが上がらない
|
| 114 |
+
- `--force` を付けて差分上書き
|
| 115 |
+
- 5GB 超えるなら一旦 tar.gz → put → 中で展開する関数を作る方が安定
|
| 116 |
+
|
| 117 |
+
### `Secret 'hf_token' not found`
|
| 118 |
+
Modal 上に `hf_token` という名前で secret が無い場合に出る。
|
| 119 |
+
[setup.md](setup.md) の Hugging Face シークレット節を参照して作成、または
|
| 120 |
+
`modal_app.py` の `from_name("hf_token", ...)` を既存 secret 名に書き換え。
|
| 121 |
+
|
| 122 |
+
### HF upload で `401 Unauthorized`
|
| 123 |
+
`hf_token` secret が read-only token の場合。write 権限が必要 (repo create / upload):
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
modal secret create HF_TOKEN_WRITE HF_TOKEN_WRITE=hf_xxxxx
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
`modal_app.py::upload_lora_to_hf` が `HF_TOKEN_WRITE` secret を参照する。
|
| 130 |
+
詳細は [setup.md](setup.md#hf-hub-への-upload-lora-配布など)。
|
| 131 |
+
|
| 132 |
+
### Modal `workspace billing cycle spend limit reached`
|
| 133 |
+
新規 app 起動時:
|
| 134 |
+
```
|
| 135 |
+
App creation failed: workspace billing cycle spend limit reached
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
**現在進行中の cloud function にも SIGINT が送られて停止する**(KeyboardInterrupt として cloud log に出る、ローカル CLI の Ctrl-C と区別不可)。先に並列起動した training が **両方** とも preempt される事例あり (2026-05-18 DMD2 v1 と DMDX v1 並列起動時)。
|
| 139 |
+
|
| 140 |
+
対処:
|
| 141 |
+
1. https://modal.com/settings/billing で **Spend limits** タブを確認
|
| 142 |
+
2. current cycle の limit を引き上げる、または cycle reset 待ち
|
| 143 |
+
3. 引き上げ後は新規 app 起動も既存 detach app も復旧
|
| 144 |
+
|
| 145 |
+
予防:
|
| 146 |
+
- 並列で高負荷 training を起動する前に Modal dashboard で remaining budget 確認
|
| 147 |
+
- DMD2 + DMDX 並列は B200 1 つあたり ~$30 × 2 = $60+ 高速消費するので、limit に余裕が必要
|
| 148 |
+
|
| 149 |
+
### 並列訓練時の rate 低下
|
| 150 |
+
本日実測 (2026-05-19、DMD2 resume + DMDX 並列):
|
| 151 |
+
- DMD2 単独: 3.66s/outer
|
| 152 |
+
- DMD2 並列 (with DMDX): **6.2s/outer** (1.7x 遅い)
|
| 153 |
+
- DMDX 単独 (smoke): 3.45s/outer
|
| 154 |
+
- DMDX 並列: 3.6s/outer (slowdown 小さい)
|
| 155 |
+
|
| 156 |
+
原因推定: Modal が異なる B200 instance を割当てるが、混雑度や近隣 workload で per-step rate に差。DMDX は forward 数が少ない (2 disc + 1 gen = 3 stage vs DMD2 の 5+1=6 stage) ので絶対的に slowdown 耐性が高い。
|
| 157 |
+
|
| 158 |
+
並列予算は単独の 1.5-2x で見積もる (e.g., DMD2 単独 $30 → 並列時 $50-60)。
|
docs/pcm.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PCM (Phased Consistency Model) 蒸留
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md) — 関連: [DMD2](dmd2.md) / [DMDX](dmdx.md) / [蒸留全般](distillation.md)
|
| 4 |
+
|
| 5 |
+
## 概要
|
| 6 |
+
|
| 7 |
+
[Wang et al. NeurIPS 2024](https://arxiv.org/abs/2405.18407)、G-U-N/Phased-Consistency-Model SD3 流派を Anima v1.0 base に移植。**FlowMatch + v-pred** で Anima の rectified flow と math 一致、ε↔v 変換不要。
|
| 8 |
+
|
| 9 |
+
**実装**: `scripts/distill/{pcm_scheduler,train_pcm}.py`
|
| 10 |
+
**Modal**: `modal_app.py::train_pcm_distill`
|
| 11 |
+
|
| 12 |
+
## 設計の鍵
|
| 13 |
+
|
| 14 |
+
- num_euler_timesteps **N=50** を K=4 phase に均等分割 → 4-step inference に最適化
|
| 15 |
+
- ランダム index sampling → phase 終端まで Euler 1 step 進める
|
| 16 |
+
- pseudo-Huber loss (`c=1e-3`) で stable convergence
|
| 17 |
+
- 1 grad-through + 3 no_grad forward / step → ~60-80 GB on B200 (LoRA-only)
|
| 18 |
+
- CFG-augmentation: `w ∈ [4.0, 5.0]` を訓練に embed → 推論 CFG=1.0 で teacher CFG=4.5 効果
|
| 19 |
+
|
| 20 |
+
## 訓練設定 (実走、2026-05-18)
|
| 21 |
+
|
| 22 |
+
| 項目 | 値 |
|
| 23 |
+
|---|---|
|
| 24 |
+
| Base | `anima-base-v1.0.safetensors` |
|
| 25 |
+
| Method | Phased Consistency Model |
|
| 26 |
+
| LoRA target | wide (AdaLN + attn + MLP の全 Linear、980 keys) |
|
| 27 |
+
| LoRA rank | 32 |
|
| 28 |
+
| Total steps | 5000 (sample_every=500) |
|
| 29 |
+
| Batch / grad accum | 1 / 4 |
|
| 30 |
+
| Resolution | 768 |
|
| 31 |
+
| Euler N / Phases K | 50 / 4 |
|
| 32 |
+
| sigma_shift | 3.0 |
|
| 33 |
+
| CFG-aug w range | [4.0, 5.0] |
|
| 34 |
+
| Huber c | 1e-3 |
|
| 35 |
+
| LR | 5e-6 |
|
| 36 |
+
| Optimizer | AdamW (wd 0.01) |
|
| 37 |
+
| Warm-start | **無し** (cold-start on v1.0 base) |
|
| 38 |
+
| GPU | B200 |
|
| 39 |
+
| 訓練時間 | **~3.4h** (1.72s/step、想定 6.5h の半分) |
|
| 40 |
+
| コスト | **~$22** (B200 $6.25/h) |
|
| 41 |
+
|
| 42 |
+
## Loss 推移
|
| 43 |
+
|
| 44 |
+
- 早期 (step 0-100): 0.001-0.14 範囲、warm-start 無しでも安定
|
| 45 |
+
- 中盤 (step 1000-3500): 0.001-0.007 で healthy plateau
|
| 46 |
+
- 後半 (step 4000-5000): 同水準維持、divergence なし
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
step 20 loss=0.0013 (初期 random spike 後すぐ低下)
|
| 50 |
+
step 130 loss=0.4468 (single spike、shifted sampling の hard spot)
|
| 51 |
+
step 500-4500 loss=0.001-0.007 範囲で平均化
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
mean collapse・NaN・OOM の発生 **なし**。
|
| 55 |
+
|
| 56 |
+
## 検証結果 (4-step 生成)
|
| 57 |
+
|
| 58 |
+
同 prompt × seed=42、Anima v1.0 base、er_sde / simple / sigma_shift=3.0:
|
| 59 |
+
|
| 60 |
+
| ckpt | sampler / scheduler | 時間 | 評価 |
|
| 61 |
+
|---|---|---|---|
|
| 62 |
+
| step 3500 | er_sde + simple | 8.8s | ✅ 2 キャラ正確分離、softer 構図 |
|
| 63 |
+
| step 4000 | res_multistep + beta | 11.2s | ✅ 同等品質、anime tone 強め |
|
| 64 |
+
| final (step 5000) | — | — | HF model card のサンプル参照 |
|
| 65 |
+
|
| 66 |
+
両 sampler とも 4-step CFG=1.0 で破綻なし。`er_sde + simple` を default 推奨 (Anima 公式準拠)。
|
| 67 |
+
|
| 68 |
+
## 配布
|
| 69 |
+
|
| 70 |
+
[**HF: darask0/anima-distill-loras/pcm/**](https://huggingface.co/darask0/anima-distill-loras/tree/main/pcm)
|
| 71 |
+
|
| 72 |
+
- `pcm_final_peft.safetensors` (diffusers / peft 用)
|
| 73 |
+
- `pcm_final_comfy.safetensors` (ComfyUI 用、980 keys)
|
| 74 |
+
- README.md (使い方 + 訓練詳細)
|
| 75 |
+
- samples/ (step 3500 / 4000 検証画像)
|
| 76 |
+
|
| 77 |
+
## ComfyUI 推奨設定
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
LoraLoaderModelOnly:
|
| 81 |
+
lora_name: pcm_final_comfy.safetensors
|
| 82 |
+
strength_model: 1.0
|
| 83 |
+
|
| 84 |
+
ModelSamplingAuraFlow:
|
| 85 |
+
shift: 3.0
|
| 86 |
+
|
| 87 |
+
KSampler:
|
| 88 |
+
steps: 4
|
| 89 |
+
cfg: 1.0
|
| 90 |
+
sampler_name: er_sde (or res_multistep)
|
| 91 |
+
scheduler: simple (or beta with res_multistep)
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## 知見
|
| 95 |
+
|
| 96 |
+
- **cold-start でも安定** — 他手法 (DMD2/LADD) は warm-start 必須だが PCM は phase consistency が anchor になる
|
| 97 |
+
- **rate 想定より速い** — README 試算 4.7s/step → 実測 1.72s/step (B200 sageattention 効果含む)
|
| 98 |
+
- **single grad-through で memory 余裕** — DMD2 の dual adapter 構成より軽量
|
docs/setup.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 事前準備
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md)
|
| 4 |
+
|
| 5 |
+
## 1. Modal セットアップ
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install modal
|
| 9 |
+
modal token new
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
## 2. Hugging Face シークレット(必須)
|
| 13 |
+
|
| 14 |
+
`modal_app.py` は Modal 上の secret 名 `hf_token`(キー: `HF_TOKEN`)を参照する。
|
| 15 |
+
既に作成済みであれば追加作業は不要。新規の場合:
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
modal secret create hf_token HF_TOKEN=hf_xxxxxxxxxxxxx
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
トークンは https://huggingface.co/settings/tokens で発行。Anima は public モデルなので
|
| 22 |
+
レート制限回避のために推奨。secret 名を変えたい場合は `modal_app.py` の
|
| 23 |
+
`from_name("hf_token", ...)` を書き換える。
|
| 24 |
+
|
| 25 |
+
### HF Hub への upload (LoRA 配布など)
|
| 26 |
+
|
| 27 |
+
`hf_token` は read 専用のことが多い。配布 / repo 作成には別途 write 権限のトークンを
|
| 28 |
+
`HF_TOKEN_WRITE` secret で持たせる:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
modal secret create HF_TOKEN_WRITE HF_TOKEN_WRITE=hf_xxxxxx_write_xxxxx
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
`modal_app.py` の `upload_lora_to_hf` がこの secret を参照する。
|
| 35 |
+
|
| 36 |
+
## 3. データセットの用意
|
| 37 |
+
|
| 38 |
+
Modal に上げる前に手元で:
|
| 39 |
+
|
| 40 |
+
```
|
| 41 |
+
my_images/
|
| 42 |
+
├── 0001.png
|
| 43 |
+
├── 0001.txt # 画像と同名 .txt にタグ/キャプション
|
| 44 |
+
├── 0002.png
|
| 45 |
+
├── 0002.txt
|
| 46 |
+
└── ...
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
タグは Danbooru 風 (", " 区切り) を推奨。WD-EVA02-Tagger v3 などで
|
| 50 |
+
自動付与してから手で軽くクリーンするのが現実的。**この時点では
|
| 51 |
+
品質タグ・年タグ・メタタグを書いていても OK**(後で
|
| 52 |
+
`clean_captions.py` が全部抜く)。
|
| 53 |
+
|
| 54 |
+
**推奨枚数**: 3,000〜8,000 枚で十分。
|
| 55 |
+
このプロジェクトの Phase 1 は「**品質タグ依存性を消すだけ**」がゴールであり、
|
| 56 |
+
審美の方向性は変えない (= base モデルが本来出していた品質を tag なしで再現)。
|
| 57 |
+
そのため:
|
| 58 |
+
- 自分の "好み" でキュレーションする必要はない
|
| 59 |
+
- Danbooru `score≥150` など **品質フィルタだけ** かけてランダムサンプル
|
| 60 |
+
- artist タグも残す (`drop_artist_prob=0` デフォルト)
|
| 61 |
+
- 多様性 > 美的偏向(モデルの汎用性を保つため)
|
docs/workflow.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 実行手順 + GPU + コスト概算
|
| 2 |
+
|
| 3 |
+
[← README に戻る](../README.md) — 前提: [setup.md](setup.md)
|
| 4 |
+
|
| 5 |
+
## Step 0: モデルダウンロード(初回のみ)
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
cd anima_modal
|
| 9 |
+
modal run modal_app.py::download_models
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
`anima-preview3-base.safetensors` (4.18GB) + Qwen3 + VAE が
|
| 13 |
+
`anima-models` Volume に保存される。
|
| 14 |
+
|
| 15 |
+
## Step 1: データセットを用意
|
| 16 |
+
|
| 17 |
+
### 1a. self-distillation で自動生成(おすすめ、約 11h / $28)
|
| 18 |
+
|
| 19 |
+
Anima base 自身に生成させて quality タグ依存性を消す pair を作る。
|
| 20 |
+
[scripts/gen_prompts.txt](../scripts/gen_prompts.txt) の 100 プロンプト × 50 seed で
|
| 21 |
+
5,000 枚生成。クライアント側でデータ用意不要。
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
# 試走: ComfyUI 起動 + 10 枚生成 (~10分, $0.10)
|
| 25 |
+
modal run modal_app.py::generate_dataset --max-images 10
|
| 26 |
+
|
| 27 |
+
# 本番(シリアル A100): 5,000 枚 (12.5h, $31)
|
| 28 |
+
modal run --detach modal_app.py::generate_dataset
|
| 29 |
+
|
| 30 |
+
# 本番(B200 × 10 並列): 5,000 枚 (~40分, $44) ← 速度優先
|
| 31 |
+
modal run --detach modal_app.py::generate_dataset_parallel
|
| 32 |
+
|
| 33 |
+
# パラメータ調整可
|
| 34 |
+
modal run --detach modal_app.py::generate_dataset \
|
| 35 |
+
--seeds-per-prompt 80 --max-images 8000
|
| 36 |
+
|
| 37 |
+
# 途中から再開 (既存 .png はスキップ)
|
| 38 |
+
modal run --detach modal_app.py::generate_dataset --start-from 50
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
ワークフローは [scripts/anima_workflow.json](../scripts/anima_workflow.json)、
|
| 42 |
+
**Anima 公式 README の推奨設定**:
|
| 43 |
+
- Sampler: `er_sde` / Scheduler: `simple` / Steps: 30 / CFG: 4.5
|
| 44 |
+
- ModelSamplingAuraFlow shift: 3.0 (公式 `anima_comparison.json` 由来)
|
| 45 |
+
- Positive prefix: `masterpiece, best quality, score_7, safe`
|
| 46 |
+
- Negative: `worst quality, low quality, score_1, score_2, score_3, artist name`
|
| 47 |
+
- タグ順: `[quality/meta/safety] [1girl/1boy] [character] [series] [artist] [general]`
|
| 48 |
+
|
| 49 |
+
### 1b. ローカル画像を持ち込み
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
modal volume put anima-dataset ./my_images /raw
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
`/dataset/raw/` 以下に展開される。
|
| 56 |
+
|
| 57 |
+
## Step 2: キャプションを掃除
|
| 58 |
+
|
| 59 |
+
このプロジェクトの Phase 1 は審美シフトではないので **artist タグは残す**:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
modal run modal_app.py::clean_captions
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
artist タグを drop するオプションは「Anima 平均スタイル化」用で、
|
| 66 |
+
今回の目的(タグ依存性除去のみ)では基本不要:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# 必要なら確率 drop / 完全 drop も可能
|
| 70 |
+
modal run modal_app.py::clean_captions --drop-artist-prob 0.5
|
| 71 |
+
modal run modal_app.py::clean_captions --keep-artist false
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
`/dataset/cleaned/` に処理後の画像と .txt が出る。中を確認:
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
modal volume get anima-dataset cleaned/0001.txt
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
`masterpiece, best quality, score_9, year 2025, newest, highres, safe`
|
| 81 |
+
が綺麗に消えていればOK。
|
| 82 |
+
|
| 83 |
+
## Step 3: Phase 1 学習(quality タグ依存性除去)
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
modal run --detach modal_app.py::train_phase1
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
`--detach` で**バックグラウンド実行**(数時間続くため)。
|
| 90 |
+
Modal ダッシュボード(modal.com/apps)で進捗確認できる。
|
| 91 |
+
|
| 92 |
+
**目安時間 (A100-80GB)**:
|
| 93 |
+
| データセット | 1 epoch | 2 epoch | コスト |
|
| 94 |
+
|---|---|---|---|
|
| 95 |
+
| 3,000 枚 | 2 h | 4 h | **$10** |
|
| 96 |
+
| 5,000 枚 | 3 h | 6-7 h | **$15-18** |
|
| 97 |
+
| 10,000 枚 | 6-7 h | 12-14 h | **$30-35** |
|
| 98 |
+
|
| 99 |
+
デフォルトは `epochs = 2`(prior shift には十分)。H100 にすれば 1.4-1.6x 速い。
|
| 100 |
+
|
| 101 |
+
**設定変更したい時**: `configs/phase1_anima.toml` を編集して
|
| 102 |
+
`modal run` を再実行(Modal が image を rebuild する)。
|
| 103 |
+
|
| 104 |
+
主な調整点:
|
| 105 |
+
- `epochs`: 試走 1、デフォルト 2、念のため 3 まで
|
| 106 |
+
(それ以上は base 性能を損なうリスク)
|
| 107 |
+
- `[optimizer].lr`: 1e-5 → 効きが弱ければ 2e-5
|
| 108 |
+
- `[adapter].rank`: 32-64 で十分(概念追加でないので大きくしても無駄)
|
| 109 |
+
- `gradient_accumulation_steps`: 5k 枚以下なら 2 で十分
|
| 110 |
+
|
| 111 |
+
## Step 4: Phase 1 結果の検証
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
modal run modal_app.py::generate_samples
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
`scripts/eval_prompts.txt` の短い「品質タグなし」プロンプトで生成して
|
| 118 |
+
`/output/phase1_samples/` に保存。
|
| 119 |
+
※ 現状は ComfyUI ベースで生成するスケルトンなので、本番では
|
| 120 |
+
ComfyUI を Modal の別 function で立てて API 経由で呼ぶ構成を別途追加することを想定。
|
| 121 |
+
|
| 122 |
+
## Step 5: Phase 2 蒸留
|
| 123 |
+
|
| 124 |
+
**数万円予算なら 5a (merge) を強く推奨**。5b/5c は数十時間の試行錯誤前提。
|
| 125 |
+
|
| 126 |
+
### 5a. Turbo LoRA との weight merge (即動く、$0.5、10分)
|
| 127 |
+
|
| 128 |
+
```bash
|
| 129 |
+
# Civitai の Turbo LoRA URL を指定して 1 ファイルに合成
|
| 130 |
+
modal run modal_app.py::merge_turbo_lora \
|
| 131 |
+
--turbo-url "https://civitai.com/api/download/models/<id>"
|
| 132 |
+
|
| 133 |
+
# alpha 調整 (デフォルトはどちらも 1.0)
|
| 134 |
+
modal run modal_app.py::merge_turbo_lora \
|
| 135 |
+
--turbo-url "..." --alpha-phase1 1.0 --alpha-turbo 0.8
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
候補の Turbo LoRA:
|
| 139 |
+
- 公式 Anima Turbo: https://civitai.com/models/2560840 (CFG=1, 8-12 step)
|
| 140 |
+
- RDBT-Anima (DMD2 蒸留先行例): https://civitai.com/models/2364703
|
| 141 |
+
|
| 142 |
+
出力は `/output/merged/anima_phase1_plus_turbo.safetensors` (LoRA だけの
|
| 143 |
+
ファイル)。ComfyUI で base + この LoRA を組めば即推論可能。
|
| 144 |
+
|
| 145 |
+
### 5b. SOTA 自前蒸留(Decoupled DMD2 + TSCD + R3GAN、B200 — **未完成、要追加開発**)
|
| 146 |
+
|
| 147 |
+
`scripts/distill/` に実装あり。**ただし 5 回試行して全て失敗**。詳細は
|
| 148 |
+
[distillation.md](distillation.md) を参照。これから取り組む人は、まず
|
| 149 |
+
そのドキュメントの §1.1 / §4 / §5 を読むことを強く推奨。
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
# (動くが収束しない) 我々が試したコマンド:
|
| 153 |
+
modal run --detach modal_app.py::generate_dataset_parallel # データ生成 $44
|
| 154 |
+
modal run --detach modal_app.py::train_sota_distill --phase b --total-steps 2000 # 失敗
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
> **強い推奨**: 商用要件なしなら 5a で十分。Anima の Turbo LoRA は **CircleStone
|
| 158 |
+
> Labs 公式** が **遥かに多い計算資源と試行錯誤** で作っており、数万円の予算で
|
| 159 |
+
> これを超える自前蒸留は現実的に困難。
|
| 160 |
+
|
| 161 |
+
## GPU の選び方
|
| 162 |
+
|
| 163 |
+
| GPU | Modal 料金 | 速度 | 推奨用途 |
|
| 164 |
+
|---|---|---|---|
|
| 165 |
+
| A100-40GB | $2.10/hr | 遅 | OOM のリスク。非推奨 |
|
| 166 |
+
| **A100-80GB** | **$2.50/hr** | 標準 | **Phase 1 デフォルト** |
|
| 167 |
+
| L40S 48GB | $1.95/hr | やや遅 | Phase 1 試走に使える |
|
| 168 |
+
| H100 80GB | $3.95/hr | 1.4-1.6x 速 | 時間最優先 / Phase 2 DMD2 |
|
| 169 |
+
| H200 | H100 と同額 | さらに速 | Modal が自動で割り当てる場合あり |
|
| 170 |
+
|
| 171 |
+
`modal_app.py` の `gpu="A100-80GB"` を書き換えるだけで切替可能。
|
| 172 |
+
|
| 173 |
+
## コスト概算 (1USD ≒ 150円換算)
|
| 174 |
+
|
| 175 |
+
### プラン A: ミニマム検証 (約 5,000円 / $33)
|
| 176 |
+
|
| 177 |
+
| 項目 | 想定 | コスト |
|
| 178 |
+
|---|---|---|
|
| 179 |
+
| Modal Volume | 10GB × 1ヶ月 | $0.90 |
|
| 180 |
+
| モデル DL (CPU 並列) | 5-10 分 | $0.01 |
|
| 181 |
+
| キャプション掃除 (CPU) | 5 分 | ~$0.00 |
|
| 182 |
+
| Phase 1 学習 (A100-80GB, 2,000枚 × 3 epoch) | 8 時間 | $20 |
|
| 183 |
+
| 検証生成 (L40S) | 10 分 | $0.30 |
|
| 184 |
+
| Phase 2 merge | 10 分 | $0.50 |
|
| 185 |
+
| 余裕枠 (失敗 retry 1 回ぶん) | | ~$10 |
|
| 186 |
+
| **合計** | | **~$32** |
|
| 187 |
+
|
| 188 |
+
### プラン B: スタンダード (約 1〜1.2万円 / $70-80)
|
| 189 |
+
|
| 190 |
+
| 項目 | 想定 | コスト |
|
| 191 |
+
|---|---|---|
|
| 192 |
+
| Modal Volume | 20GB × 1ヶ月 | $1.80 |
|
| 193 |
+
| **Phase 1 (A100-80GB, 10,000枚 × 3 epoch)** | 18-21 時間 | **$45-53** |
|
| 194 |
+
| 検証 + merge + 試行 retry | | ~$5 |
|
| 195 |
+
| 余裕 | | ~$15 |
|
| 196 |
+
| **合計** | | **~$70** |
|
| 197 |
+
|
| 198 |
+
### プラン C: 余裕プラン / 蒸留も自前 (約 3〜4万円 / $200-270)
|
| 199 |
+
|
| 200 |
+
| 項目 | 想定 | コスト |
|
| 201 |
+
|---|---|---|
|
| 202 |
+
| Phase 1 (A100-80GB, 10k × 5 epoch) | 30-35 時間 | $75-88 |
|
| 203 |
+
| Phase 2 LCM 自前蒸留 (A100, 試行2回) | 20 時間 | $50 |
|
| 204 |
+
| ↑ DMD2 にする場合 (H100, 試行1回) | 15 時間 | $60 |
|
| 205 |
+
| Volume + 検証 + 余裕 | | ~$30 |
|
| 206 |
+
| **合計** | | **$200-270** |
|
| 207 |
+
|
| 208 |
+
> ⚠️ `--detach` で背景実行中も課金は続く。`modal app stop rapid-anima` で
|
| 209 |
+
> 即停止できるので、想定時間を超えたら必ず確認。`cleanup_checkpoints` で
|
| 210 |
+
> Volume 課金 ($0.15/GB/月) も適時間引き。
|
modal_app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modal Image 側でインストール済み。ローカル開発時用。
|
| 2 |
+
modal>=0.66
|
| 3 |
+
huggingface_hub>=0.27
|
| 4 |
+
Pillow>=10.0
|
samples/dmd2_step500.png
ADDED
|
Git LFS Details
|
samples/dmdx_step500.png
ADDED
|
Git LFS Details
|
scripts/anima_finetune_dataset_workflow.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Fine-tune 用データセット生成 workflow。base=hakushiMixAnima_v02 + LoRA=anima-highres-aesthetic-boost strength 1.0、Anima 公式推奨 30 step CFG=4.5 er_sde simple、ModelSamplingAuraFlow shift=3.0 込み。node ID は scripts/generate_dataset.py の patch_workflow hardcode に合わせて positive=5 latent=7 KSampler=8。",
|
| 3 |
+
"1": {
|
| 4 |
+
"class_type": "UNETLoader",
|
| 5 |
+
"inputs": {
|
| 6 |
+
"unet_name": "hakushiMixAnima_v02.safetensors",
|
| 7 |
+
"weight_dtype": "default"
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"2": {
|
| 11 |
+
"class_type": "ModelSamplingAuraFlow",
|
| 12 |
+
"inputs": {
|
| 13 |
+
"model": ["1", 0],
|
| 14 |
+
"shift": 3.0
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"2b": {
|
| 18 |
+
"class_type": "LoraLoaderModelOnly",
|
| 19 |
+
"inputs": {
|
| 20 |
+
"model": ["2", 0],
|
| 21 |
+
"lora_name": "anima-highres-aesthetic-boost.safetensors",
|
| 22 |
+
"strength_model": 1.0
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"3": {
|
| 26 |
+
"class_type": "CLIPLoader",
|
| 27 |
+
"inputs": {
|
| 28 |
+
"clip_name": "qwen_3_06b_base.safetensors",
|
| 29 |
+
"type": "stable_diffusion",
|
| 30 |
+
"device": "default"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"4": {
|
| 34 |
+
"class_type": "CLIPTextEncode",
|
| 35 |
+
"inputs": {
|
| 36 |
+
"clip": ["3", 0],
|
| 37 |
+
"text": "worst quality, low quality, score_1, score_2, score_3, score_4, blurry, deformed, bad anatomy, extra limbs, fused fingers, artist name, text, watermark, lowres, jpeg artifacts, censored"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"5": {
|
| 41 |
+
"class_type": "CLIPTextEncode",
|
| 42 |
+
"inputs": {
|
| 43 |
+
"clip": ["3", 0],
|
| 44 |
+
"text": "$PROMPT"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"6": {
|
| 48 |
+
"class_type": "VAELoader",
|
| 49 |
+
"inputs": {
|
| 50 |
+
"vae_name": "qwen_image_vae.safetensors"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"7": {
|
| 54 |
+
"class_type": "EmptyLatentImage",
|
| 55 |
+
"inputs": {
|
| 56 |
+
"width": "$WIDTH",
|
| 57 |
+
"height": "$HEIGHT",
|
| 58 |
+
"batch_size": 1
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"8": {
|
| 62 |
+
"class_type": "KSampler",
|
| 63 |
+
"inputs": {
|
| 64 |
+
"model": ["2b", 0],
|
| 65 |
+
"positive": ["5", 0],
|
| 66 |
+
"negative": ["4", 0],
|
| 67 |
+
"latent_image": ["7", 0],
|
| 68 |
+
"seed": "$SEED",
|
| 69 |
+
"steps": 30,
|
| 70 |
+
"cfg": 4.5,
|
| 71 |
+
"sampler_name": "er_sde",
|
| 72 |
+
"scheduler": "simple",
|
| 73 |
+
"denoise": 1.0
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"9": {
|
| 77 |
+
"class_type": "VAEDecode",
|
| 78 |
+
"inputs": {
|
| 79 |
+
"samples": ["8", 0],
|
| 80 |
+
"vae": ["6", 0]
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"10": {
|
| 84 |
+
"class_type": "SaveImage",
|
| 85 |
+
"inputs": {
|
| 86 |
+
"filename_prefix": "finetune_data",
|
| 87 |
+
"images": ["9", 0]
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
scripts/anima_verify_workflow.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "ユーザー指定 Anima_simple.json + Anima 公式必須の ModelSamplingAuraFlow shift=3.0 + LoraLoaderModelOnly 追加版。node ID は scripts/generate_dataset.py の patch_workflow が hardcode (positive=5, latent=7, sampler=8) するため、それに合わせて配置。LoRA を使わない場合は LoraLoaderModelOnly の strength_model を 0.0 にする。",
|
| 3 |
+
"1": {
|
| 4 |
+
"class_type": "UNETLoader",
|
| 5 |
+
"inputs": {
|
| 6 |
+
"unet_name": "anima-base-v1.0.safetensors",
|
| 7 |
+
"weight_dtype": "default"
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"2": {
|
| 11 |
+
"class_type": "ModelSamplingAuraFlow",
|
| 12 |
+
"inputs": {
|
| 13 |
+
"model": ["1", 0],
|
| 14 |
+
"shift": 3.0
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"2b": {
|
| 18 |
+
"class_type": "LoraLoaderModelOnly",
|
| 19 |
+
"inputs": {
|
| 20 |
+
"model": ["2", 0],
|
| 21 |
+
"lora_name": "anima_turbo.safetensors",
|
| 22 |
+
"strength_model": 1.0
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"3": {
|
| 26 |
+
"class_type": "CLIPLoader",
|
| 27 |
+
"inputs": {
|
| 28 |
+
"clip_name": "qwen_3_06b_base.safetensors",
|
| 29 |
+
"type": "stable_diffusion",
|
| 30 |
+
"device": "default"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"4": {
|
| 34 |
+
"class_type": "CLIPTextEncode",
|
| 35 |
+
"inputs": {
|
| 36 |
+
"clip": ["3", 0],
|
| 37 |
+
"text": "worst quality, low quality, score_1, score_2, score_3, score_4, blurry, deformed, bad anatomy, extra limbs, fused fingers, artist name, text, watermark, lowres, jpeg artifacts, censored"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"5": {
|
| 41 |
+
"class_type": "CLIPTextEncode",
|
| 42 |
+
"inputs": {
|
| 43 |
+
"clip": ["3", 0],
|
| 44 |
+
"text": "$PROMPT"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"6": {
|
| 48 |
+
"class_type": "VAELoader",
|
| 49 |
+
"inputs": {
|
| 50 |
+
"vae_name": "qwen_image_vae.safetensors"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"7": {
|
| 54 |
+
"class_type": "EmptyLatentImage",
|
| 55 |
+
"inputs": {
|
| 56 |
+
"width": "$WIDTH",
|
| 57 |
+
"height": "$HEIGHT",
|
| 58 |
+
"batch_size": 1
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"8": {
|
| 62 |
+
"class_type": "KSampler",
|
| 63 |
+
"inputs": {
|
| 64 |
+
"model": ["2b", 0],
|
| 65 |
+
"positive": ["5", 0],
|
| 66 |
+
"negative": ["4", 0],
|
| 67 |
+
"latent_image": ["7", 0],
|
| 68 |
+
"seed": "$SEED",
|
| 69 |
+
"steps": 30,
|
| 70 |
+
"cfg": 4.5,
|
| 71 |
+
"sampler_name": "er_sde",
|
| 72 |
+
"scheduler": "simple",
|
| 73 |
+
"denoise": 1.0
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"9": {
|
| 77 |
+
"class_type": "VAEDecode",
|
| 78 |
+
"inputs": {
|
| 79 |
+
"samples": ["8", 0],
|
| 80 |
+
"vae": ["6", 0]
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"10": {
|
| 84 |
+
"class_type": "SaveImage",
|
| 85 |
+
"inputs": {
|
| 86 |
+
"filename_prefix": "anima_verify",
|
| 87 |
+
"images": ["9", 0]
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
scripts/anima_workflow.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Anima ComfyUI workflow (API format). Used by generate_dataset.py. Placeholders: $PROMPT, $WIDTH, $HEIGHT, $SEED are substituted at submit time.",
|
| 3 |
+
"1": {
|
| 4 |
+
"class_type": "UNETLoader",
|
| 5 |
+
"inputs": {
|
| 6 |
+
"unet_name": "anima-base-v1.0.safetensors",
|
| 7 |
+
"weight_dtype": "default"
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"2": {
|
| 11 |
+
"class_type": "ModelSamplingAuraFlow",
|
| 12 |
+
"inputs": {
|
| 13 |
+
"model": ["1", 0],
|
| 14 |
+
"shift": 3.0
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"3": {
|
| 18 |
+
"class_type": "CLIPLoader",
|
| 19 |
+
"inputs": {
|
| 20 |
+
"clip_name": "qwen_3_06b_base.safetensors",
|
| 21 |
+
"type": "stable_diffusion",
|
| 22 |
+
"device": "default"
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"4": {
|
| 26 |
+
"class_type": "VAELoader",
|
| 27 |
+
"inputs": {
|
| 28 |
+
"vae_name": "qwen_image_vae.safetensors"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"5": {
|
| 32 |
+
"class_type": "CLIPTextEncode",
|
| 33 |
+
"inputs": {
|
| 34 |
+
"text": "$PROMPT",
|
| 35 |
+
"clip": ["3", 0]
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"6": {
|
| 39 |
+
"class_type": "CLIPTextEncode",
|
| 40 |
+
"inputs": {
|
| 41 |
+
"text": "worst quality, low quality, score_1, score_2, score_3, artist name",
|
| 42 |
+
"clip": ["3", 0]
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"7": {
|
| 46 |
+
"class_type": "EmptyLatentImage",
|
| 47 |
+
"inputs": {
|
| 48 |
+
"width": "$WIDTH",
|
| 49 |
+
"height": "$HEIGHT",
|
| 50 |
+
"batch_size": 1
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"8": {
|
| 54 |
+
"class_type": "KSampler",
|
| 55 |
+
"inputs": {
|
| 56 |
+
"model": ["2", 0],
|
| 57 |
+
"positive": ["5", 0],
|
| 58 |
+
"negative": ["6", 0],
|
| 59 |
+
"latent_image": ["7", 0],
|
| 60 |
+
"seed": "$SEED",
|
| 61 |
+
"steps": 30,
|
| 62 |
+
"cfg": 4.5,
|
| 63 |
+
"sampler_name": "er_sde",
|
| 64 |
+
"scheduler": "simple",
|
| 65 |
+
"denoise": 1.0
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"9": {
|
| 69 |
+
"class_type": "VAEDecode",
|
| 70 |
+
"inputs": {
|
| 71 |
+
"samples": ["8", 0],
|
| 72 |
+
"vae": ["4", 0]
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"10": {
|
| 76 |
+
"class_type": "SaveImage",
|
| 77 |
+
"inputs": {
|
| 78 |
+
"images": ["9", 0],
|
| 79 |
+
"filename_prefix": "anima_gen"
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
scripts/anima_workflow_phase_a.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Phase A 蒸留出力 (gen_final.safetensors) を base として使う workflow。LoRA 不要、CFG=1、step 数は --override-steps で切替可。",
|
| 3 |
+
"1": {
|
| 4 |
+
"class_type": "UNETLoader",
|
| 5 |
+
"inputs": {
|
| 6 |
+
"unet_name": "phase_a_distilled.safetensors",
|
| 7 |
+
"weight_dtype": "default"
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"2": {
|
| 11 |
+
"class_type": "ModelSamplingAuraFlow",
|
| 12 |
+
"inputs": {
|
| 13 |
+
"model": ["1", 0],
|
| 14 |
+
"shift": 3.0
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"3": {
|
| 18 |
+
"class_type": "CLIPLoader",
|
| 19 |
+
"inputs": {
|
| 20 |
+
"clip_name": "qwen_3_06b_base.safetensors",
|
| 21 |
+
"type": "stable_diffusion",
|
| 22 |
+
"device": "default"
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"4": {
|
| 26 |
+
"class_type": "VAELoader",
|
| 27 |
+
"inputs": {
|
| 28 |
+
"vae_name": "qwen_image_vae.safetensors"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"5": {
|
| 32 |
+
"class_type": "CLIPTextEncode",
|
| 33 |
+
"inputs": {
|
| 34 |
+
"text": "$PROMPT",
|
| 35 |
+
"clip": ["3", 0]
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"6": {
|
| 39 |
+
"class_type": "CLIPTextEncode",
|
| 40 |
+
"inputs": {
|
| 41 |
+
"text": "",
|
| 42 |
+
"clip": ["3", 0]
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"7": {
|
| 46 |
+
"class_type": "EmptyLatentImage",
|
| 47 |
+
"inputs": {
|
| 48 |
+
"width": "$WIDTH",
|
| 49 |
+
"height": "$HEIGHT",
|
| 50 |
+
"batch_size": 1
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"8": {
|
| 54 |
+
"class_type": "KSampler",
|
| 55 |
+
"inputs": {
|
| 56 |
+
"model": ["2", 0],
|
| 57 |
+
"positive": ["5", 0],
|
| 58 |
+
"negative": ["6", 0],
|
| 59 |
+
"latent_image": ["7", 0],
|
| 60 |
+
"seed": "$SEED",
|
| 61 |
+
"steps": 8,
|
| 62 |
+
"cfg": 1.0,
|
| 63 |
+
"sampler_name": "er_sde",
|
| 64 |
+
"scheduler": "simple",
|
| 65 |
+
"denoise": 1.0
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"9": {
|
| 69 |
+
"class_type": "VAEDecode",
|
| 70 |
+
"inputs": {
|
| 71 |
+
"samples": ["8", 0],
|
| 72 |
+
"vae": ["4", 0]
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"10": {
|
| 76 |
+
"class_type": "SaveImage",
|
| 77 |
+
"inputs": {
|
| 78 |
+
"images": ["9", 0],
|
| 79 |
+
"filename_prefix": "anima_phase_a"
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
scripts/anima_workflow_phase_a_step1000.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Phase A step 1000 checkpoint で動作確認用",
|
| 3 |
+
"1": {
|
| 4 |
+
"class_type": "UNETLoader",
|
| 5 |
+
"inputs": {
|
| 6 |
+
"unet_name": "phase_a_step1000.safetensors",
|
| 7 |
+
"weight_dtype": "default"
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"2": {
|
| 11 |
+
"class_type": "ModelSamplingAuraFlow",
|
| 12 |
+
"inputs": {"model": ["1", 0], "shift": 3.0}
|
| 13 |
+
},
|
| 14 |
+
"3": {
|
| 15 |
+
"class_type": "CLIPLoader",
|
| 16 |
+
"inputs": {"clip_name": "qwen_3_06b_base.safetensors", "type": "stable_diffusion", "device": "default"}
|
| 17 |
+
},
|
| 18 |
+
"4": {"class_type": "VAELoader", "inputs": {"vae_name": "qwen_image_vae.safetensors"}},
|
| 19 |
+
"5": {"class_type": "CLIPTextEncode", "inputs": {"text": "$PROMPT", "clip": ["3", 0]}},
|
| 20 |
+
"6": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["3", 0]}},
|
| 21 |
+
"7": {"class_type": "EmptyLatentImage", "inputs": {"width": "$WIDTH", "height": "$HEIGHT", "batch_size": 1}},
|
| 22 |
+
"8": {
|
| 23 |
+
"class_type": "KSampler",
|
| 24 |
+
"inputs": {
|
| 25 |
+
"model": ["2", 0], "positive": ["5", 0], "negative": ["6", 0], "latent_image": ["7", 0],
|
| 26 |
+
"seed": "$SEED", "steps": 8, "cfg": 1.0,
|
| 27 |
+
"sampler_name": "er_sde", "scheduler": "simple", "denoise": 1.0
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["4", 0]}},
|
| 31 |
+
"10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": "anima_step1000"}}
|
| 32 |
+
}
|
scripts/anima_workflow_phase_b.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Phase B 4-step distillation LoRA + Anima base. CFG=1, 4 step (Phase C で 2 step も試す予定)",
|
| 3 |
+
"1": {
|
| 4 |
+
"class_type": "UNETLoader",
|
| 5 |
+
"inputs": {"unet_name": "anima-base-v1.0.safetensors", "weight_dtype": "default"}
|
| 6 |
+
},
|
| 7 |
+
"2": {
|
| 8 |
+
"class_type": "ModelSamplingAuraFlow",
|
| 9 |
+
"inputs": {"model": ["1", 0], "shift": 3.0}
|
| 10 |
+
},
|
| 11 |
+
"2b": {
|
| 12 |
+
"class_type": "LoraLoaderModelOnly",
|
| 13 |
+
"inputs": {
|
| 14 |
+
"model": ["2", 0],
|
| 15 |
+
"lora_name": "phase_b_4step_lora.safetensors",
|
| 16 |
+
"strength_model": 1.0
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"3": {
|
| 20 |
+
"class_type": "CLIPLoader",
|
| 21 |
+
"inputs": {"clip_name": "qwen_3_06b_base.safetensors", "type": "stable_diffusion", "device": "default"}
|
| 22 |
+
},
|
| 23 |
+
"4": {"class_type": "VAELoader", "inputs": {"vae_name": "qwen_image_vae.safetensors"}},
|
| 24 |
+
"5": {"class_type": "CLIPTextEncode", "inputs": {"text": "$PROMPT", "clip": ["3", 0]}},
|
| 25 |
+
"6": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["3", 0]}},
|
| 26 |
+
"7": {"class_type": "EmptyLatentImage", "inputs": {"width": "$WIDTH", "height": "$HEIGHT", "batch_size": 1}},
|
| 27 |
+
"8": {
|
| 28 |
+
"class_type": "KSampler",
|
| 29 |
+
"inputs": {
|
| 30 |
+
"model": ["2b", 0], "positive": ["5", 0], "negative": ["6", 0], "latent_image": ["7", 0],
|
| 31 |
+
"seed": "$SEED", "steps": 4, "cfg": 1.0,
|
| 32 |
+
"sampler_name": "er_sde", "scheduler": "simple", "denoise": 1.0
|
| 33 |
+
}
|
| 34 |
+
},
|
| 35 |
+
"9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["4", 0]}},
|
| 36 |
+
"10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": "anima_phase_b"}}
|
| 37 |
+
}
|
scripts/anima_workflow_turbo.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Anima + Turbo LoRA workflow. 8 step, CFG 1 (Civitai 公式推奨)。$PROMPT/$WIDTH/$HEIGHT/$SEED は generate_dataset.py が置換。",
|
| 3 |
+
"1": {
|
| 4 |
+
"class_type": "UNETLoader",
|
| 5 |
+
"inputs": {
|
| 6 |
+
"unet_name": "anima-base-v1.0.safetensors",
|
| 7 |
+
"weight_dtype": "default"
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"2": {
|
| 11 |
+
"class_type": "ModelSamplingAuraFlow",
|
| 12 |
+
"inputs": {
|
| 13 |
+
"model": ["1", 0],
|
| 14 |
+
"shift": 3.0
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"2b": {
|
| 18 |
+
"class_type": "LoraLoaderModelOnly",
|
| 19 |
+
"inputs": {
|
| 20 |
+
"model": ["2", 0],
|
| 21 |
+
"lora_name": "anima_turbo.safetensors",
|
| 22 |
+
"strength_model": 1.0
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"3": {
|
| 26 |
+
"class_type": "CLIPLoader",
|
| 27 |
+
"inputs": {
|
| 28 |
+
"clip_name": "qwen_3_06b_base.safetensors",
|
| 29 |
+
"type": "stable_diffusion",
|
| 30 |
+
"device": "default"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"4": {
|
| 34 |
+
"class_type": "VAELoader",
|
| 35 |
+
"inputs": {
|
| 36 |
+
"vae_name": "qwen_image_vae.safetensors"
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
"5": {
|
| 40 |
+
"class_type": "CLIPTextEncode",
|
| 41 |
+
"inputs": {
|
| 42 |
+
"text": "$PROMPT",
|
| 43 |
+
"clip": ["3", 0]
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"6": {
|
| 47 |
+
"class_type": "CLIPTextEncode",
|
| 48 |
+
"inputs": {
|
| 49 |
+
"text": "",
|
| 50 |
+
"clip": ["3", 0]
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"7": {
|
| 54 |
+
"class_type": "EmptyLatentImage",
|
| 55 |
+
"inputs": {
|
| 56 |
+
"width": "$WIDTH",
|
| 57 |
+
"height": "$HEIGHT",
|
| 58 |
+
"batch_size": 1
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"8": {
|
| 62 |
+
"class_type": "KSampler",
|
| 63 |
+
"inputs": {
|
| 64 |
+
"model": ["2b", 0],
|
| 65 |
+
"positive": ["5", 0],
|
| 66 |
+
"negative": ["6", 0],
|
| 67 |
+
"latent_image": ["7", 0],
|
| 68 |
+
"seed": "$SEED",
|
| 69 |
+
"steps": 8,
|
| 70 |
+
"cfg": 1.0,
|
| 71 |
+
"sampler_name": "er_sde",
|
| 72 |
+
"scheduler": "simple",
|
| 73 |
+
"denoise": 1.0
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"9": {
|
| 77 |
+
"class_type": "VAEDecode",
|
| 78 |
+
"inputs": {
|
| 79 |
+
"samples": ["8", 0],
|
| 80 |
+
"vae": ["4", 0]
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"10": {
|
| 84 |
+
"class_type": "SaveImage",
|
| 85 |
+
"inputs": {
|
| 86 |
+
"images": ["9", 0],
|
| 87 |
+
"filename_prefix": "anima_turbo"
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
scripts/clean_captions.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
キャプション掃除スクリプト
|
| 4 |
+
==========================
|
| 5 |
+
|
| 6 |
+
Anima 公式が推奨する以下のタグを ".txt" キャプションから除去:
|
| 7 |
+
- Quality (Human): masterpiece, best quality, good quality, normal quality, low quality, worst quality
|
| 8 |
+
- Quality (PonyV7 score): score_9, score_8, ..., score_1
|
| 9 |
+
- Time period: year 2025, year 2024, ..., newest, recent, mid, early, old
|
| 10 |
+
- Meta: highres, absurdres, anime screenshot, jpeg artifacts, official art,
|
| 11 |
+
lowres, blurry, watermark, signature, web address, artist name,
|
| 12 |
+
censored, uncensored 等
|
| 13 |
+
|
| 14 |
+
これらをデータから抜くことで、学習後のモデルは「タグなし = 高品質」を学ぶ。
|
| 15 |
+
|
| 16 |
+
使い方:
|
| 17 |
+
python clean_captions.py --input ./raw --output ./cleaned \
|
| 18 |
+
[--drop-artist-prob 0.0] [--drop-all-artists]
|
| 19 |
+
"""
|
| 20 |
+
import argparse
|
| 21 |
+
import re
|
| 22 |
+
import shutil
|
| 23 |
+
import random
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# 削除対象タグ (大文字小文字無視、前後空白許容)
|
| 28 |
+
QUALITY_TAGS_HUMAN = {
|
| 29 |
+
"masterpiece", "best quality", "good quality",
|
| 30 |
+
"normal quality", "low quality", "worst quality",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
QUALITY_TAGS_SCORE = {f"score_{i}" for i in range(1, 10)}
|
| 34 |
+
|
| 35 |
+
# 公式 README より: 期間タグ
|
| 36 |
+
PERIOD_TAGS = {"newest", "recent", "mid", "early", "old"}
|
| 37 |
+
|
| 38 |
+
# Year タグは正規表現で吸収
|
| 39 |
+
YEAR_RE = re.compile(r"^year\s+(19|20)\d{2}$", re.IGNORECASE)
|
| 40 |
+
|
| 41 |
+
META_TAGS = {
|
| 42 |
+
"highres", "absurdres", "lowres",
|
| 43 |
+
"anime screenshot", "official art",
|
| 44 |
+
"jpeg artifacts", "blurry", "bad quality",
|
| 45 |
+
"watermark", "signature", "web address", "twitter username",
|
| 46 |
+
"artist name", "logo",
|
| 47 |
+
"censored", "uncensored", "mosaic censoring", "bar censor",
|
| 48 |
+
"safe", "sensitive", "nsfw", "explicit", # safety タグも除去 (要なら別フラグに)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
FIXED_DROP = (
|
| 52 |
+
{t.lower() for t in QUALITY_TAGS_HUMAN}
|
| 53 |
+
| QUALITY_TAGS_SCORE
|
| 54 |
+
| PERIOD_TAGS
|
| 55 |
+
| META_TAGS
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def should_drop(tag: str) -> bool:
|
| 60 |
+
t = tag.strip().lower()
|
| 61 |
+
if not t:
|
| 62 |
+
return True
|
| 63 |
+
if t in FIXED_DROP:
|
| 64 |
+
return True
|
| 65 |
+
if YEAR_RE.match(t):
|
| 66 |
+
return True
|
| 67 |
+
return False
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def clean_caption(
|
| 71 |
+
text: str,
|
| 72 |
+
drop_artist_prob: float = 0.0,
|
| 73 |
+
drop_all_artists: bool = False,
|
| 74 |
+
rng: random.Random | None = None,
|
| 75 |
+
) -> str:
|
| 76 |
+
rng = rng or random.Random()
|
| 77 |
+
# ", " 区切りを基本に、Danbooru の "_" 区切りも空白へ統一されている前提
|
| 78 |
+
parts = [p.strip() for p in text.split(",")]
|
| 79 |
+
out = []
|
| 80 |
+
for p in parts:
|
| 81 |
+
if should_drop(p):
|
| 82 |
+
continue
|
| 83 |
+
# artist タグは "@xxx" 形式 (Anima 仕様)
|
| 84 |
+
if p.startswith("@"):
|
| 85 |
+
if drop_all_artists:
|
| 86 |
+
continue
|
| 87 |
+
if drop_artist_prob > 0 and rng.random() < drop_artist_prob:
|
| 88 |
+
continue
|
| 89 |
+
out.append(p)
|
| 90 |
+
return ", ".join(out)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def main():
|
| 94 |
+
ap = argparse.ArgumentParser()
|
| 95 |
+
ap.add_argument("--input", required=True, type=Path)
|
| 96 |
+
ap.add_argument("--output", required=True, type=Path)
|
| 97 |
+
ap.add_argument(
|
| 98 |
+
"--drop-artist-prob",
|
| 99 |
+
type=float,
|
| 100 |
+
default=0.0,
|
| 101 |
+
help="artist タグ(@xxx)をランダムに drop する確率 (0.0〜1.0)",
|
| 102 |
+
)
|
| 103 |
+
ap.add_argument(
|
| 104 |
+
"--drop-all-artists",
|
| 105 |
+
action="store_true",
|
| 106 |
+
help="artist タグを全部除去 (Anima 平均スタイル化)",
|
| 107 |
+
)
|
| 108 |
+
ap.add_argument(
|
| 109 |
+
"--exts",
|
| 110 |
+
default="png,jpg,jpeg,webp",
|
| 111 |
+
help="画像拡張子(カンマ区切り)",
|
| 112 |
+
)
|
| 113 |
+
args = ap.parse_args()
|
| 114 |
+
|
| 115 |
+
args.output.mkdir(parents=True, exist_ok=True)
|
| 116 |
+
exts = tuple("." + e.lower() for e in args.exts.split(","))
|
| 117 |
+
rng = random.Random(42)
|
| 118 |
+
|
| 119 |
+
n_img, n_cap, n_missing = 0, 0, 0
|
| 120 |
+
for img in args.input.rglob("*"):
|
| 121 |
+
if not img.is_file() or not img.suffix.lower() in exts:
|
| 122 |
+
continue
|
| 123 |
+
n_img += 1
|
| 124 |
+
rel = img.relative_to(args.input)
|
| 125 |
+
out_img = args.output / rel
|
| 126 |
+
out_img.parent.mkdir(parents=True, exist_ok=True)
|
| 127 |
+
if not out_img.exists():
|
| 128 |
+
shutil.copy(img, out_img)
|
| 129 |
+
|
| 130 |
+
cap_in = img.with_suffix(".txt")
|
| 131 |
+
cap_out = out_img.with_suffix(".txt")
|
| 132 |
+
if not cap_in.exists():
|
| 133 |
+
n_missing += 1
|
| 134 |
+
cap_out.write_text("", encoding="utf-8")
|
| 135 |
+
continue
|
| 136 |
+
|
| 137 |
+
text = cap_in.read_text(encoding="utf-8", errors="ignore")
|
| 138 |
+
cleaned = clean_caption(
|
| 139 |
+
text,
|
| 140 |
+
drop_artist_prob=args.drop_artist_prob,
|
| 141 |
+
drop_all_artists=args.drop_all_artists,
|
| 142 |
+
rng=rng,
|
| 143 |
+
)
|
| 144 |
+
cap_out.write_text(cleaned, encoding="utf-8")
|
| 145 |
+
n_cap += 1
|
| 146 |
+
|
| 147 |
+
print(f"[clean_captions] images={n_img} captions={n_cap} missing_txt={n_missing}")
|
| 148 |
+
print(f"[clean_captions] output -> {args.output}")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
main()
|
scripts/compare_prompts.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase A 完了後の比較用 5 prompt (sample_prompts.txt から抜粋、絶対 index は本 file 内 0..4)
|
| 2 |
+
1boy, short blonde hair, green eyes, knight armor, castle hallway, holding shield and sword, torchlight, noble pose
|
| 3 |
+
2girls, friends, one with long brown hair other with short blonde, casual clothes, shopping together, mall interior, laughing, holding shopping bags
|
| 4 |
+
a fluffy white cat, curled up on windowsill, sunlight, lace curtains, peaceful sleep, soft focus background
|
| 5 |
+
fantasy landscape, floating islands with waterfalls, sky bridges, sunset, magical atmosphere, distant mountains
|
| 6 |
+
magical spellcasting scene, glowing rune circle, swirling energy, dramatic shadows, fantasy atmosphere, ethereal
|
scripts/distill/__init__.py
ADDED
|
File without changes
|
scripts/distill/anima_ladd_disc.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Anima LADD discriminator (AMD Nitro-1 流派、PixArt → Anima 移植)
|
| 3 |
+
|
| 4 |
+
設計の根拠 (R3GAN 失敗との対比):
|
| 5 |
+
- R3GAN は CNN-on-latent を一から学習 → Anima 16ch latent の prior を持たず崩壊
|
| 6 |
+
- LADD は teacher の frozen MiniTrainDIT を **feature extractor** として再利用
|
| 7 |
+
teacher は Anima latent を理解済 → head は判別だけ学習すればよい
|
| 8 |
+
- 加えて spectral norm + BatchNormLocal で D 自体の安定性も確保
|
| 9 |
+
|
| 10 |
+
Architecture:
|
| 11 |
+
- D backbone = teacher MiniTrainDIT (frozen) を deepcopy
|
| 12 |
+
- forward 時に N 個の block 出力を hook で集める (multi-scale)
|
| 13 |
+
- 各 hook 出力に DiscHead を 1 個ずつ通して logits を得る
|
| 14 |
+
- 最終: list[Tensor] of logits を返し、loss 側で BCE
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
from typing import List, Sequence
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
from torch.nn.utils import spectral_norm
|
| 23 |
+
|
| 24 |
+
from .anima_loader import AnimaBundle
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ----- BatchNormLocal -------------------------------------------------------
|
| 28 |
+
class BatchNormLocal(nn.Module):
|
| 29 |
+
"""各 sample 内で per-channel normalize。BatchNorm のような分布依存性なし。
|
| 30 |
+
AMD Nitro-1 で D の安定化に効いた知見。"""
|
| 31 |
+
def __init__(self, channels: int, eps: float = 1e-5):
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.eps = eps
|
| 34 |
+
self.weight = nn.Parameter(torch.ones(channels))
|
| 35 |
+
self.bias = nn.Parameter(torch.zeros(channels))
|
| 36 |
+
|
| 37 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 38 |
+
# x: (B, C, L) or (B, C)
|
| 39 |
+
if x.dim() == 2:
|
| 40 |
+
mean = x.mean(dim=1, keepdim=True)
|
| 41 |
+
var = x.var(dim=1, keepdim=True, unbiased=False)
|
| 42 |
+
else:
|
| 43 |
+
mean = x.mean(dim=2, keepdim=True)
|
| 44 |
+
var = x.var(dim=2, keepdim=True, unbiased=False)
|
| 45 |
+
x = (x - mean) / torch.sqrt(var + self.eps)
|
| 46 |
+
if x.dim() == 2:
|
| 47 |
+
return x * self.weight + self.bias
|
| 48 |
+
return x * self.weight.view(1, -1, 1) + self.bias.view(1, -1, 1)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ----- DiscHead -------------------------------------------------------------
|
| 52 |
+
class DiscHead(nn.Module):
|
| 53 |
+
"""1 つの hooked feature を scalar logit に。
|
| 54 |
+
SpectralConv1d → BNL → LeakyReLU → residual block → Linear projection
|
| 55 |
+
"""
|
| 56 |
+
def __init__(self, in_dim: int, hidden: int = 512):
|
| 57 |
+
super().__init__()
|
| 58 |
+
# in: (B, L, D) → permute → (B, D, L)
|
| 59 |
+
self.conv1 = spectral_norm(nn.Conv1d(in_dim, hidden, kernel_size=1))
|
| 60 |
+
self.bnl1 = BatchNormLocal(hidden)
|
| 61 |
+
self.act1 = nn.LeakyReLU(0.2, inplace=True)
|
| 62 |
+
|
| 63 |
+
self.conv2 = spectral_norm(nn.Conv1d(hidden, hidden, kernel_size=9, padding=4, padding_mode="circular"))
|
| 64 |
+
self.bnl2 = BatchNormLocal(hidden)
|
| 65 |
+
self.act2 = nn.LeakyReLU(0.2, inplace=True)
|
| 66 |
+
|
| 67 |
+
self.proj = spectral_norm(nn.Conv1d(hidden, 1, kernel_size=1))
|
| 68 |
+
|
| 69 |
+
def forward(self, feat: torch.Tensor) -> torch.Tensor:
|
| 70 |
+
"""
|
| 71 |
+
feat: (B, L, D) ※L = num tokens、D = channel dim
|
| 72 |
+
return: (B,) per-sample logit (mean over L)
|
| 73 |
+
"""
|
| 74 |
+
x = feat.transpose(1, 2) # (B, D, L)
|
| 75 |
+
h = self.act1(self.bnl1(self.conv1(x)))
|
| 76 |
+
h = h + self.act2(self.bnl2(self.conv2(h)))
|
| 77 |
+
logits = self.proj(h) # (B, 1, L)
|
| 78 |
+
return logits.mean(dim=2).squeeze(1) # (B,)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ----- AnimaLADDDiscriminator ----------------------------------------------
|
| 82 |
+
class AnimaLADDDiscriminator(nn.Module):
|
| 83 |
+
"""teacher MiniTrainDIT を frozen で抱え、forward 時に N ブロックの出力を
|
| 84 |
+
hook で集めて N 個の DiscHead に通す。head の出力 (logits) を list で返す。
|
| 85 |
+
|
| 86 |
+
teacher が Anima latent (16ch × 128² → 1024 token) を理解しているので、
|
| 87 |
+
head は判別だけを学習。スクラッチ CNN より遥かに stable。
|
| 88 |
+
"""
|
| 89 |
+
def __init__(
|
| 90 |
+
self,
|
| 91 |
+
teacher_transformer: nn.Module,
|
| 92 |
+
block_ids: Sequence[int] = (2, 8, 14, 20, 26),
|
| 93 |
+
head_hidden: int = 512,
|
| 94 |
+
):
|
| 95 |
+
super().__init__()
|
| 96 |
+
# teacher 本体は frozen で保持 (forward に使う)
|
| 97 |
+
# 重要: deepcopy はしない。teacher_transformer はすでに caller 側で
|
| 98 |
+
# frozen な状態で与えられる前提 (gen の teacher と共有 OK、forward は no_grad)
|
| 99 |
+
self.teacher = teacher_transformer
|
| 100 |
+
for p in self.teacher.parameters():
|
| 101 |
+
p.requires_grad = False
|
| 102 |
+
|
| 103 |
+
# block_ids が teacher の block 数を超えていないか
|
| 104 |
+
blocks = self._find_blocks()
|
| 105 |
+
n_blocks = len(blocks)
|
| 106 |
+
self.block_ids = [bid for bid in block_ids if bid < n_blocks]
|
| 107 |
+
print(f"[ladd-D] teacher has {n_blocks} blocks; hooking {self.block_ids}")
|
| 108 |
+
|
| 109 |
+
# 各 hook 出力の channel 数を 1 度 dummy forward で取って head を作成
|
| 110 |
+
self._hook_outputs: dict[int, torch.Tensor] = {}
|
| 111 |
+
self._handles = []
|
| 112 |
+
for bid in self.block_ids:
|
| 113 |
+
h = blocks[bid].register_forward_hook(self._make_hook(bid))
|
| 114 |
+
self._handles.append(h)
|
| 115 |
+
|
| 116 |
+
# heads は dummy forward 後に lazy init
|
| 117 |
+
self.heads = nn.ModuleDict()
|
| 118 |
+
self.head_hidden = head_hidden
|
| 119 |
+
|
| 120 |
+
@torch.no_grad()
|
| 121 |
+
def lazy_init_heads(self, sample_x: torch.Tensor, sample_t: torch.Tensor, sample_cond: torch.Tensor):
|
| 122 |
+
"""heads を 1 度 dummy forward して channel 数を確定させてから build。"""
|
| 123 |
+
self._hook_outputs.clear()
|
| 124 |
+
_ = self._teacher_forward(sample_x, sample_t, sample_cond)
|
| 125 |
+
# 各 hook の出力 shape を見て head を作る
|
| 126 |
+
for bid in self.block_ids:
|
| 127 |
+
out = self._hook_outputs[bid]
|
| 128 |
+
# out: (B, L, D) を想定 (MiniTrainDIT の block 出力)
|
| 129 |
+
if out.dim() == 4: # (B, T, L, D) など video 系
|
| 130 |
+
out = out.flatten(1, -2) # (B, T*L, D)
|
| 131 |
+
B, L, D = out.shape
|
| 132 |
+
print(f"[ladd-D] block {bid} feature shape: B={B} L={L} D={D}")
|
| 133 |
+
head = DiscHead(in_dim=D, hidden=self.head_hidden)
|
| 134 |
+
self.heads[str(bid)] = head
|
| 135 |
+
self._hook_outputs.clear()
|
| 136 |
+
|
| 137 |
+
def _find_blocks(self) -> list[nn.Module]:
|
| 138 |
+
"""MiniTrainDIT 内部の **DiT 本体 block list** を見つける。
|
| 139 |
+
Anima/Cosmos の DiT blocks は ~28 個、llm_adapter.blocks は 6 個程度なので
|
| 140 |
+
区別する必要がある。優先順位:
|
| 141 |
+
1. transformer.blocks (top-level、direct attribute) を最優先
|
| 142 |
+
2. それ以外で、llm_adapter を含まない blocks ModuleList で最大のもの
|
| 143 |
+
"""
|
| 144 |
+
# 1) 最優先: top-level の `.blocks`
|
| 145 |
+
if hasattr(self.teacher, "blocks") and isinstance(self.teacher.blocks, nn.ModuleList):
|
| 146 |
+
n = len(self.teacher.blocks)
|
| 147 |
+
print(f"[ladd-D] found top-level teacher.blocks ({n} blocks)")
|
| 148 |
+
return list(self.teacher.blocks)
|
| 149 |
+
|
| 150 |
+
# 2) llm_adapter を含まない候補から最大のものを採用
|
| 151 |
+
candidates: list[tuple[str, list[nn.Module]]] = []
|
| 152 |
+
for name, module in self.teacher.named_modules():
|
| 153 |
+
if not isinstance(module, nn.ModuleList):
|
| 154 |
+
continue
|
| 155 |
+
if "block" not in name.lower():
|
| 156 |
+
continue
|
| 157 |
+
if "llm_adapter" in name:
|
| 158 |
+
continue # text encoder の bridge ブロックを除外
|
| 159 |
+
if len(module) >= 4:
|
| 160 |
+
candidates.append((name, list(module)))
|
| 161 |
+
|
| 162 |
+
if not candidates:
|
| 163 |
+
raise RuntimeError(
|
| 164 |
+
"Could not find DiT blocks ModuleList. Inspected teacher module tree but "
|
| 165 |
+
"found no non-llm_adapter `blocks`-named list with >=4 children."
|
| 166 |
+
)
|
| 167 |
+
# 最大数の block list を採用 (DiT 本体 28+ blocks > llm_adapter の 6 blocks)
|
| 168 |
+
candidates.sort(key=lambda x: -len(x[1]))
|
| 169 |
+
name, children = candidates[0]
|
| 170 |
+
print(f"[ladd-D] selected block list at: {name} ({len(children)} blocks)")
|
| 171 |
+
return children
|
| 172 |
+
|
| 173 |
+
def _make_hook(self, bid: int):
|
| 174 |
+
def _h(module, inputs, output):
|
| 175 |
+
# output の正規化: tuple なら最初の要素、5D なら 3D に flatten
|
| 176 |
+
if isinstance(output, tuple):
|
| 177 |
+
out = output[0]
|
| 178 |
+
else:
|
| 179 |
+
out = output
|
| 180 |
+
if out.dim() == 5:
|
| 181 |
+
# (B, T, H, W, D) → (B, T*H*W, D)
|
| 182 |
+
B = out.size(0)
|
| 183 |
+
D = out.size(-1)
|
| 184 |
+
out = out.reshape(B, -1, D)
|
| 185 |
+
elif out.dim() == 4:
|
| 186 |
+
# (B, H, W, D) → (B, H*W, D)
|
| 187 |
+
B = out.size(0)
|
| 188 |
+
D = out.size(-1)
|
| 189 |
+
out = out.reshape(B, -1, D)
|
| 190 |
+
self._hook_outputs[bid] = out
|
| 191 |
+
return _h
|
| 192 |
+
|
| 193 |
+
def _teacher_forward(self, x: torch.Tensor, t: torch.Tensor, cond: torch.Tensor):
|
| 194 |
+
"""teacher を forward して hook を発火させる。返り値は使わない (hook が中身)。"""
|
| 195 |
+
return AnimaBundle.dit_forward(self.teacher, x, t, cond)
|
| 196 |
+
|
| 197 |
+
def forward(
|
| 198 |
+
self, x: torch.Tensor, t: torch.Tensor, cond: torch.Tensor,
|
| 199 |
+
gradient_to_input: bool = False,
|
| 200 |
+
) -> List[torch.Tensor]:
|
| 201 |
+
"""各 head の logit を list で返す。
|
| 202 |
+
|
| 203 |
+
gradient_to_input=True (G phase で adv loss が student を訓練するために必要):
|
| 204 |
+
teacher は frozen だが、forward を grad-on で走らせ activation を残す。
|
| 205 |
+
そうすると logits の grad が teacher backbone を通って x → student LoRA に届く。
|
| 206 |
+
gradient_to_input=False (D phase: heads だけ訓練、x は固定):
|
| 207 |
+
teacher を no_grad で走らせ、features を detach (省メモリ + 安全)。
|
| 208 |
+
"""
|
| 209 |
+
self._hook_outputs.clear()
|
| 210 |
+
if gradient_to_input:
|
| 211 |
+
# teacher params は frozen なので weights は更新されないが、
|
| 212 |
+
# activations が残り gradient は input → student に流れる
|
| 213 |
+
_ = self._teacher_forward(x, t, cond)
|
| 214 |
+
else:
|
| 215 |
+
with torch.no_grad():
|
| 216 |
+
_ = self._teacher_forward(x, t, cond)
|
| 217 |
+
logits = []
|
| 218 |
+
for bid in self.block_ids:
|
| 219 |
+
feat = self._hook_outputs[bid]
|
| 220 |
+
if not gradient_to_input:
|
| 221 |
+
feat = feat.detach().requires_grad_(False)
|
| 222 |
+
# feat の dtype を head と揃える (head は fp32、bf16 混在を避ける)
|
| 223 |
+
head = self.heads[str(bid)]
|
| 224 |
+
head_dtype = next(head.parameters()).dtype
|
| 225 |
+
logits.append(head(feat.to(dtype=head_dtype)))
|
| 226 |
+
self._hook_outputs.clear()
|
| 227 |
+
return logits
|
| 228 |
+
|
| 229 |
+
def trainable_parameters(self):
|
| 230 |
+
return list(self.heads.parameters())
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# ----- losses ---------------------------------------------------------------
|
| 234 |
+
def ladd_d_loss(
|
| 235 |
+
logits_real: List[torch.Tensor], logits_fake: List[torch.Tensor],
|
| 236 |
+
) -> tuple[torch.Tensor, dict]:
|
| 237 |
+
"""BCE on each head, sum across heads."""
|
| 238 |
+
loss_real, loss_fake = 0.0, 0.0
|
| 239 |
+
for lr in logits_real:
|
| 240 |
+
loss_real = loss_real + F.binary_cross_entropy_with_logits(
|
| 241 |
+
lr, torch.ones_like(lr)
|
| 242 |
+
)
|
| 243 |
+
for lf in logits_fake:
|
| 244 |
+
loss_fake = loss_fake + F.binary_cross_entropy_with_logits(
|
| 245 |
+
lf, torch.zeros_like(lf)
|
| 246 |
+
)
|
| 247 |
+
loss = (loss_real + loss_fake) / max(len(logits_real), 1)
|
| 248 |
+
return loss, {
|
| 249 |
+
"l_d_real": loss_real.detach() / max(len(logits_real), 1),
|
| 250 |
+
"l_d_fake": loss_fake.detach() / max(len(logits_fake), 1),
|
| 251 |
+
"l_d_total": loss.detach(),
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def ladd_g_adv_loss(logits_fake: List[torch.Tensor]) -> tuple[torch.Tensor, dict]:
|
| 256 |
+
"""non-saturating: BCE with label=1 (D に real と思わせる)"""
|
| 257 |
+
loss = 0.0
|
| 258 |
+
for lf in logits_fake:
|
| 259 |
+
loss = loss + F.binary_cross_entropy_with_logits(
|
| 260 |
+
lf, torch.ones_like(lf)
|
| 261 |
+
)
|
| 262 |
+
loss = loss / max(len(logits_fake), 1)
|
| 263 |
+
return loss, {"l_g_adv": loss.detach()}
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def ladd_g_recon_loss(
|
| 267 |
+
x0_hat: torch.Tensor, x0_teacher: torch.Tensor,
|
| 268 |
+
) -> tuple[torch.Tensor, dict]:
|
| 269 |
+
"""smooth L1 = Huber loss、mean collapse 防止の anchor。"""
|
| 270 |
+
loss = F.smooth_l1_loss(x0_hat.float(), x0_teacher.detach().float())
|
| 271 |
+
return loss, {"l_g_recon": loss.detach()}
|
scripts/distill/anima_loader.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Anima loader: diffusion-pipe の cosmos_predict2 Pipeline を活用して
|
| 3 |
+
DiT / VAE / Qwen3 text encoder / LLM adapter をロードする薄いラッパ。
|
| 4 |
+
|
| 5 |
+
diffusion-pipe 本体には sampling/inference 関数が無いので、ここで
|
| 6 |
+
- text_encode(): prompts -> crossattn_emb (LLM adapter 通過後)
|
| 7 |
+
- vae_encode(): pixels -> latents
|
| 8 |
+
- vae_decode(): latents -> pixels
|
| 9 |
+
- velocity(): DiT forward (rectified flow velocity prediction)
|
| 10 |
+
- add_noise(): noisy = (1-t)*latents + t*noise (rectified flow forward)
|
| 11 |
+
- euler_step(): inference 用 1-step 前進
|
| 12 |
+
までを実装する。
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# diffusion-pipe を import path に強制配置。
|
| 25 |
+
# 重要: site-packages に top-level `utils` を持つ別パッケージがある場合、
|
| 26 |
+
# それが namespace package 化して shadow するため、(1) /workspace/diffusion-pipe を
|
| 27 |
+
# 先頭に固定 (2) cached `utils` モジュールを sys.modules から除外 する。
|
| 28 |
+
_DPIPE = Path("/workspace/diffusion-pipe")
|
| 29 |
+
if _DPIPE.exists():
|
| 30 |
+
_dp = str(_DPIPE)
|
| 31 |
+
sys.path = [_dp] + [p for p in sys.path if p != _dp]
|
| 32 |
+
# 既にロード済みの top-level utils / models をクリア(他パッケージの shadow を排除)
|
| 33 |
+
for _mod in list(sys.modules.keys()):
|
| 34 |
+
if _mod == "utils" or _mod.startswith("utils.") \
|
| 35 |
+
or _mod == "models" or _mod.startswith("models."):
|
| 36 |
+
del sys.modules[_mod]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class AnimaPaths:
|
| 41 |
+
transformer: str = "/models/checkpoints/anima-base-v1.0.safetensors"
|
| 42 |
+
vae: str = "/models/checkpoints/qwen_image_vae.safetensors"
|
| 43 |
+
llm: str = "/models/checkpoints/qwen_3_06b_base.safetensors"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class AnimaBundle:
|
| 47 |
+
"""Anima のモデル一式を保持。.transformer / .vae / .text_encoder / .tokenizer 等を直接触る。"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, pipeline, device: str | torch.device = "cuda"):
|
| 50 |
+
self.pipeline = pipeline # diffusion-pipe の CosmosPredict2Pipeline
|
| 51 |
+
self.device = torch.device(device)
|
| 52 |
+
|
| 53 |
+
self.transformer = pipeline.transformer
|
| 54 |
+
self.vae = pipeline.vae
|
| 55 |
+
self.text_encoder = pipeline.text_encoder # Qwen3Model (inner)
|
| 56 |
+
self.qwen_tokenizer = pipeline.tokenizer
|
| 57 |
+
# LLM adapter は transformer 内部にある (Anima のみ)
|
| 58 |
+
self.llm_adapter = getattr(pipeline.transformer, "llm_adapter", None)
|
| 59 |
+
self.t5_tokenizer = getattr(pipeline, "t5_tokenizer", None)
|
| 60 |
+
self.is_generic_llm = getattr(pipeline, "is_generic_llm", False)
|
| 61 |
+
self.vae_scale = pipeline.vae.scale
|
| 62 |
+
|
| 63 |
+
# ---- text encoding (Qwen3 → LLM adapter) ------------------------------
|
| 64 |
+
@torch.no_grad()
|
| 65 |
+
def text_encode(self, prompts: list[str]) -> torch.Tensor:
|
| 66 |
+
"""prompts -> crossattn_emb (B, 512, 1024) (DiT 用、LLM adapter 通過後)"""
|
| 67 |
+
# diffusion-pipe の _tokenize を再現
|
| 68 |
+
def _tok(tokenizer, prompts):
|
| 69 |
+
return tokenizer(prompts, return_tensors="pt", truncation=True,
|
| 70 |
+
padding="max_length", max_length=512)
|
| 71 |
+
|
| 72 |
+
qwen_enc = _tok(self.qwen_tokenizer, prompts)
|
| 73 |
+
input_ids = qwen_enc.input_ids.to(self.device)
|
| 74 |
+
attn_mask = qwen_enc.attention_mask.to(self.device)
|
| 75 |
+
|
| 76 |
+
outputs = self.text_encoder(input_ids=input_ids, attention_mask=attn_mask)
|
| 77 |
+
encoded = outputs.last_hidden_state
|
| 78 |
+
encoded = encoded.masked_fill(~attn_mask.bool().unsqueeze(-1), 0.0)
|
| 79 |
+
|
| 80 |
+
if self.llm_adapter is None or self.t5_tokenizer is None:
|
| 81 |
+
return encoded
|
| 82 |
+
|
| 83 |
+
t5_enc = _tok(self.t5_tokenizer, prompts)
|
| 84 |
+
t5_ids = t5_enc.input_ids.to(self.device)
|
| 85 |
+
t5_mask = t5_enc.attention_mask.to(self.device)
|
| 86 |
+
|
| 87 |
+
crossattn = self.llm_adapter(
|
| 88 |
+
source_hidden_states=encoded,
|
| 89 |
+
target_input_ids=t5_ids,
|
| 90 |
+
target_attention_mask=t5_mask,
|
| 91 |
+
source_attention_mask=attn_mask,
|
| 92 |
+
)
|
| 93 |
+
crossattn = crossattn.masked_fill(~t5_mask.bool().unsqueeze(-1), 0.0)
|
| 94 |
+
return crossattn
|
| 95 |
+
|
| 96 |
+
# ---- VAE --------------------------------------------------------------
|
| 97 |
+
@torch.no_grad()
|
| 98 |
+
def vae_encode(self, pixels: torch.Tensor) -> torch.Tensor:
|
| 99 |
+
"""pixels (B, 3, H, W) in [-1, 1] -> latents (B, 16, 1, H/8, W/8)
|
| 100 |
+
※ Anima は静止画でも T=1 の 3D latent を扱う"""
|
| 101 |
+
if pixels.dim() == 4:
|
| 102 |
+
pixels = pixels.unsqueeze(2) # (B, 3, 1, H, W)
|
| 103 |
+
# VAE の weights dtype に合わせる
|
| 104 |
+
vae_dtype = next(self.vae.model.parameters()).dtype
|
| 105 |
+
pixels = pixels.to(device=self.device, dtype=vae_dtype)
|
| 106 |
+
return self.vae.model.encode(pixels, self.vae_scale)
|
| 107 |
+
|
| 108 |
+
@torch.no_grad()
|
| 109 |
+
def vae_decode(self, latents: torch.Tensor) -> torch.Tensor:
|
| 110 |
+
"""latents (B, 16, 1, H_lat, W_lat) -> pixels (B, 3, 1, H, W) in [-1, 1]"""
|
| 111 |
+
vae_dtype = next(self.vae.model.parameters()).dtype
|
| 112 |
+
latents = latents.to(device=self.device, dtype=vae_dtype)
|
| 113 |
+
return self.vae.model.decode(latents, self.vae_scale)
|
| 114 |
+
|
| 115 |
+
# ---- DiT (rectified flow velocity prediction) -------------------------
|
| 116 |
+
def velocity(
|
| 117 |
+
self,
|
| 118 |
+
latents: torch.Tensor, # (B, 16, T, H_lat, W_lat) - noisy
|
| 119 |
+
timesteps: torch.Tensor, # (B,) in [0, 1]
|
| 120 |
+
crossattn_emb: torch.Tensor, # (B, 512, 1024)
|
| 121 |
+
padding_mask: torch.Tensor | None = None,
|
| 122 |
+
) -> torch.Tensor:
|
| 123 |
+
"""Anima DiT forward. 返り値は velocity (B, 16, T, H_lat, W_lat)。"""
|
| 124 |
+
if padding_mask is None:
|
| 125 |
+
padding_mask = self.zero_padding_mask(latents)
|
| 126 |
+
return self.transformer(
|
| 127 |
+
x_B_C_T_H_W=latents,
|
| 128 |
+
timesteps_B_T=timesteps,
|
| 129 |
+
crossattn_emb=crossattn_emb,
|
| 130 |
+
padding_mask=padding_mask,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
@staticmethod
|
| 134 |
+
def zero_padding_mask(latents: torch.Tensor) -> torch.Tensor:
|
| 135 |
+
"""MiniTrainDIT.concat_padding_mask=True なので必須。shape=(B, 1, H_lat, W_lat)。"""
|
| 136 |
+
B, _, _, H, W = latents.shape
|
| 137 |
+
return torch.zeros(B, 1, H, W, dtype=latents.dtype, device=latents.device)
|
| 138 |
+
|
| 139 |
+
@staticmethod
|
| 140 |
+
def dit_forward(
|
| 141 |
+
transformer: "torch.nn.Module",
|
| 142 |
+
latents: torch.Tensor,
|
| 143 |
+
t: torch.Tensor,
|
| 144 |
+
crossattn_emb: torch.Tensor,
|
| 145 |
+
) -> torch.Tensor:
|
| 146 |
+
"""trainer から任意の transformer (gen / guidance) を呼ぶための薄い wrapper。
|
| 147 |
+
全入力を transformer の weights dtype に揃え、padding_mask も自動生成。"""
|
| 148 |
+
# weight dtype に揃える (t は float なので broadcast で他がアップキャストされやすい)
|
| 149 |
+
w_dtype = next(transformer.parameters()).dtype
|
| 150 |
+
latents = latents.to(dtype=w_dtype)
|
| 151 |
+
t = t.to(dtype=w_dtype)
|
| 152 |
+
crossattn_emb = crossattn_emb.to(dtype=w_dtype)
|
| 153 |
+
padding_mask = AnimaBundle.zero_padding_mask(latents)
|
| 154 |
+
return transformer(
|
| 155 |
+
x_B_C_T_H_W=latents,
|
| 156 |
+
timesteps_B_T=t,
|
| 157 |
+
crossattn_emb=crossattn_emb,
|
| 158 |
+
padding_mask=padding_mask,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
# ---- rectified-flow utilities ----------------------------------------
|
| 162 |
+
@staticmethod
|
| 163 |
+
def add_noise(
|
| 164 |
+
latents: torch.Tensor, noise: torch.Tensor, t: torch.Tensor
|
| 165 |
+
) -> torch.Tensor:
|
| 166 |
+
"""forward process: x_t = (1-t)*x_0 + t*noise"""
|
| 167 |
+
t_ = t.view(-1, *([1] * (latents.dim() - 1)))
|
| 168 |
+
return (1 - t_) * latents + t_ * noise
|
| 169 |
+
|
| 170 |
+
@staticmethod
|
| 171 |
+
def velocity_target(latents: torch.Tensor, noise: torch.Tensor) -> torch.Tensor:
|
| 172 |
+
"""target velocity = noise - latents (rectified flow の training target)"""
|
| 173 |
+
return noise - latents
|
| 174 |
+
|
| 175 |
+
@staticmethod
|
| 176 |
+
def x0_from_velocity(
|
| 177 |
+
latents_t: torch.Tensor, v_pred: torch.Tensor, t: torch.Tensor
|
| 178 |
+
) -> torch.Tensor:
|
| 179 |
+
"""x_0 estimate = x_t - t * v_pred (linear FM のため)"""
|
| 180 |
+
t_ = t.view(-1, *([1] * (latents_t.dim() - 1)))
|
| 181 |
+
return latents_t - t_ * v_pred
|
| 182 |
+
|
| 183 |
+
@staticmethod
|
| 184 |
+
def euler_step(
|
| 185 |
+
latents_t: torch.Tensor,
|
| 186 |
+
v_pred: torch.Tensor,
|
| 187 |
+
t: torch.Tensor,
|
| 188 |
+
t_next: torch.Tensor,
|
| 189 |
+
) -> torch.Tensor:
|
| 190 |
+
"""Euler step: x_{t_next} = x_t + (t_next - t) * v_pred"""
|
| 191 |
+
dt = (t_next - t).view(-1, *([1] * (latents_t.dim() - 1)))
|
| 192 |
+
return latents_t + dt * v_pred
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def build_anima(
|
| 196 |
+
paths: AnimaPaths | None = None,
|
| 197 |
+
device: str | torch.device = "cuda",
|
| 198 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 199 |
+
) -> AnimaBundle:
|
| 200 |
+
"""diffusion-pipe の CosmosPredict2Pipeline を初期化して AnimaBundle で返す。
|
| 201 |
+
|
| 202 |
+
内部的には toml config を最小限作って Pipeline に渡す。
|
| 203 |
+
"""
|
| 204 |
+
import importlib
|
| 205 |
+
import importlib.util
|
| 206 |
+
_dp = str(_DPIPE)
|
| 207 |
+
|
| 208 |
+
# diffusion-pipe の utils/ と models/ に空 __init__.py を作成して regular package 化
|
| 209 |
+
for sub in ("utils", "models", "optimizers"):
|
| 210 |
+
sub_dir = os.path.join(_dp, sub)
|
| 211 |
+
init_file = os.path.join(sub_dir, "__init__.py")
|
| 212 |
+
if os.path.isdir(sub_dir) and not os.path.exists(init_file):
|
| 213 |
+
with open(init_file, "w"):
|
| 214 |
+
pass
|
| 215 |
+
|
| 216 |
+
# sys.path 強制 + cache クリア
|
| 217 |
+
sys.path = [_dp] + [p for p in sys.path if p != _dp]
|
| 218 |
+
for _mod in list(sys.modules.keys()):
|
| 219 |
+
if _mod in ("utils", "models", "optimizers") \
|
| 220 |
+
or _mod.startswith(("utils.", "models.", "optimizers.")):
|
| 221 |
+
del sys.modules[_mod]
|
| 222 |
+
|
| 223 |
+
# importlib で **明示的に** 各 package を /workspace/diffusion-pipe 配下から
|
| 224 |
+
# 読み込み、sys.modules に固定して shadow を完全排除する。
|
| 225 |
+
def _load_pkg(name: str):
|
| 226 |
+
pkg_dir = os.path.join(_dp, name)
|
| 227 |
+
init_file = os.path.join(pkg_dir, "__init__.py")
|
| 228 |
+
spec = importlib.util.spec_from_file_location(
|
| 229 |
+
name, init_file,
|
| 230 |
+
submodule_search_locations=[pkg_dir],
|
| 231 |
+
)
|
| 232 |
+
mod = importlib.util.module_from_spec(spec)
|
| 233 |
+
sys.modules[name] = mod
|
| 234 |
+
spec.loader.exec_module(mod)
|
| 235 |
+
print(f"[setup] forced load {name} from {pkg_dir}")
|
| 236 |
+
|
| 237 |
+
for _pkg in ("utils", "models", "optimizers"):
|
| 238 |
+
_load_pkg(_pkg)
|
| 239 |
+
|
| 240 |
+
# 動作確認: utils.common が解決できることを試す
|
| 241 |
+
import utils.common # noqa: F401
|
| 242 |
+
print("[setup] utils.common import OK")
|
| 243 |
+
|
| 244 |
+
from models import cosmos_predict2 # diffusion-pipe 側
|
| 245 |
+
|
| 246 |
+
paths = paths or AnimaPaths()
|
| 247 |
+
|
| 248 |
+
# CosmosPredict2Pipeline は config dict から **torch.dtype object** を期待する
|
| 249 |
+
# (TOML 読み込み時に diffusion-pipe 側で str→dtype 変換するが、ここでは直接渡す)
|
| 250 |
+
cfg = {
|
| 251 |
+
"model": {
|
| 252 |
+
"type": "anima",
|
| 253 |
+
"transformer_path": str(paths.transformer),
|
| 254 |
+
"vae_path": str(paths.vae),
|
| 255 |
+
"llm_path": str(paths.llm),
|
| 256 |
+
"dtype": dtype,
|
| 257 |
+
"transformer_dtype": dtype,
|
| 258 |
+
"timestep_sample_method": "logit_normal",
|
| 259 |
+
},
|
| 260 |
+
# adapter / optimizer 等は trainer 側で attach するので空でOK
|
| 261 |
+
"adapter": {"type": "lora", "rank": 1, "alpha": 1, "dropout": 0.0, "dtype": dtype},
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
# __init__ で VAE + text encoder + tokenizers が CPU 上に load 済み
|
| 265 |
+
pipeline = cosmos_predict2.CosmosPredict2Pipeline(cfg)
|
| 266 |
+
# DiT をロード (transformer.llm_adapter も内部で attach される)
|
| 267 |
+
pipeline.load_diffusion_model()
|
| 268 |
+
|
| 269 |
+
# device / dtype に配置
|
| 270 |
+
pipeline.transformer = pipeline.transformer.to(device=device, dtype=dtype)
|
| 271 |
+
pipeline.text_encoder = pipeline.text_encoder.to(device=device, dtype=dtype)
|
| 272 |
+
pipeline.vae.model = pipeline.vae.model.to(device=device, dtype=dtype)
|
| 273 |
+
# mean/std は __init__ で 'cuda' に moved 済 (cosmos_predict2.py:200-201)
|
| 274 |
+
|
| 275 |
+
return AnimaBundle(pipeline, device=device)
|
scripts/distill/dataset.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
キャプション付き画像データセット。/dataset/raw 以下の .png + .txt ペアを返す。
|
| 3 |
+
DMD2 では「real image を見る」のは fake-score 更新時 (の v-pred MSE) のみで
|
| 4 |
+
あって、generator/real-score 側は noise から直接サンプル → image は不要。
|
| 5 |
+
従ってここでは pixels + caption を返すシンプルな実装で十分。
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from torch.utils.data import Dataset
|
| 13 |
+
from PIL import Image
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _resize_short_side(img: Image.Image, target: int) -> Image.Image:
|
| 17 |
+
"""短辺を target に合わせる aspect-preserving resize"""
|
| 18 |
+
w, h = img.size
|
| 19 |
+
if min(w, h) == target:
|
| 20 |
+
return img
|
| 21 |
+
if w < h:
|
| 22 |
+
new_w = target
|
| 23 |
+
new_h = int(round(h * target / w))
|
| 24 |
+
else:
|
| 25 |
+
new_h = target
|
| 26 |
+
new_w = int(round(w * target / h))
|
| 27 |
+
return img.resize((new_w, new_h), Image.BICUBIC)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _center_crop(img: Image.Image, size: int) -> Image.Image:
|
| 31 |
+
w, h = img.size
|
| 32 |
+
left = (w - size) // 2
|
| 33 |
+
top = (h - size) // 2
|
| 34 |
+
return img.crop((left, top, left + size, top + size))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _to_tensor_normalize(img: Image.Image) -> torch.Tensor:
|
| 38 |
+
"""PIL RGB -> torch (3, H, W) in [-1, 1]"""
|
| 39 |
+
arr = np.asarray(img, dtype=np.float32) / 127.5 - 1.0 # (H, W, 3) in [-1, 1]
|
| 40 |
+
return torch.from_numpy(arr).permute(2, 0, 1).contiguous()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class AnimaImageCaptionDataset(Dataset):
|
| 44 |
+
"""
|
| 45 |
+
Args:
|
| 46 |
+
root: 画像/キャプションのルート。再帰的に *.png + 同名.txt を拾う。
|
| 47 |
+
resolution: 単一解像度に統一する出力サイズ(短辺合わせ → center crop)。
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
root: str | Path,
|
| 53 |
+
resolution: int = 1024,
|
| 54 |
+
exts: tuple[str, ...] = (".png", ".jpg", ".jpeg", ".webp"),
|
| 55 |
+
):
|
| 56 |
+
self.root = Path(root)
|
| 57 |
+
self.resolution = resolution
|
| 58 |
+
self.items: list[tuple[Path, Path]] = []
|
| 59 |
+
for img in sorted(self.root.rglob("*")):
|
| 60 |
+
if not img.is_file() or img.suffix.lower() not in exts:
|
| 61 |
+
continue
|
| 62 |
+
cap = img.with_suffix(".txt")
|
| 63 |
+
if cap.exists():
|
| 64 |
+
self.items.append((img, cap))
|
| 65 |
+
|
| 66 |
+
def __len__(self) -> int:
|
| 67 |
+
return len(self.items)
|
| 68 |
+
|
| 69 |
+
def __getitem__(self, idx: int) -> dict:
|
| 70 |
+
img_path, cap_path = self.items[idx]
|
| 71 |
+
img = Image.open(img_path).convert("RGB")
|
| 72 |
+
img = _resize_short_side(img, self.resolution)
|
| 73 |
+
img = _center_crop(img, self.resolution)
|
| 74 |
+
pixels = _to_tensor_normalize(img) # (3, H, W) in [-1, 1]
|
| 75 |
+
caption = cap_path.read_text(encoding="utf-8").strip()
|
| 76 |
+
return {"pixels": pixels, "caption": caption, "path": str(img_path)}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def collate_fn(batch: list[dict]) -> dict:
|
| 80 |
+
"""default collate と同じだが captions は list で保持。"""
|
| 81 |
+
pixels = torch.stack([b["pixels"] for b in batch])
|
| 82 |
+
captions = [b["caption"] for b in batch]
|
| 83 |
+
paths = [b["path"] for b in batch]
|
| 84 |
+
return {"pixels": pixels, "captions": captions, "paths": paths}
|
scripts/distill/dmd2_official_loss.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Anima 用 DMD2 + TrigFlow distillation (NVIDIA cosmos-predict2.5 official 流派)
|
| 3 |
+
|
| 4 |
+
参照: cosmos_predict2/_src/predict2/distill/models/video2world_model_distill_dmd2.py
|
| 5 |
+
(lines 165-310 の step_generator / step_critic を直接移植)
|
| 6 |
+
|
| 7 |
+
要点:
|
| 8 |
+
- TrigFlow time sampling: s ∈ shifted_uniform(shift=5) → t = arctan(s/(1-s))
|
| 9 |
+
- DMD2 gradient trick: loss = ((x_hat - (x_hat - grad).detach())**2).mean()
|
| 10 |
+
where grad = (x0_fake - x0_teacher_cfg) / |x_hat - x0_teacher_cfg|.mean()
|
| 11 |
+
- Critic phase weighted denoise: loss = ((x_hat - x0_fake)**2 / sin(t)**2).mean()
|
| 12 |
+
- Alternation: critic × N (=5), generator × 1
|
| 13 |
+
- Few-step student rollout (n=1..4), **grad は最終 step のみ** (memory efficient)
|
| 14 |
+
|
| 15 |
+
Anima の差分 (vs upstream):
|
| 16 |
+
- upstream は video2world、condition.is_video 経路あり → image only に折り畳む (T=1)
|
| 17 |
+
- LoRA-only: student/fake_score それぞれに別 PEFT adapter
|
| 18 |
+
- rectified flow (sigma_data=1.0 想定)、本 file の sCM wrapper 計算は upstream と同じ
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
import math
|
| 22 |
+
from typing import Callable
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
import torch.nn.functional as F
|
| 26 |
+
|
| 27 |
+
from .anima_loader import AnimaBundle
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ----- TrigFlow / sCM coefficient helpers (sigma_data=1.0, rectified flow) ---
|
| 31 |
+
|
| 32 |
+
def trigflow_t_to_sigma(t: torch.Tensor) -> torch.Tensor:
|
| 33 |
+
"""trigflow t ∈ (0, π/2) を rectified-flow sigma に変換。
|
| 34 |
+
sigma = tan(t)、sigma_data=1.0 で normalize。"""
|
| 35 |
+
return torch.tan(t)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def rf_s_to_trigflow_t(s: torch.Tensor) -> torch.Tensor:
|
| 39 |
+
"""rectified-flow time s ∈ [0, 1] を trigflow t = arctan(s / (1 - s)) に変換。"""
|
| 40 |
+
s = s.clamp(1e-6, 1.0 - 1e-6)
|
| 41 |
+
return torch.atan(s / (1.0 - s))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def sample_shifted_uniform_s(B: int, shift: float = 5.0, device=None, dtype=torch.float32) -> torch.Tensor:
|
| 45 |
+
"""shifted uniform on [0,1]: u ~ U(0,1) → s = shift*u / (1 + (shift-1)*u)。
|
| 46 |
+
upstream sample_shifted_uniform 相当。shift>1 で low-s (high-noise) 側にシフト。"""
|
| 47 |
+
u = torch.rand(B, device=device, dtype=dtype)
|
| 48 |
+
return shift * u / (1.0 + (shift - 1.0) * u)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def scm_coefficients(t: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 52 |
+
"""sigma_data=1.0 の RectifiedFlow_sCMWrapper:
|
| 53 |
+
c_skip = cos(t)、c_out = -sin(t)、c_in = 1 / sqrt(sigma**2 + 1) = cos(t)、c_noise = t
|
| 54 |
+
|
| 55 |
+
使い方:
|
| 56 |
+
x_t = c_in.recip() * x_t_raw (DiT 入力前の正規化、cos(t) スケール)
|
| 57 |
+
x_t_raw = c_skip * x_t + c_out * v_pred (x0 復元)
|
| 58 |
+
簡単化のため Anima では x_t を直接渡し、v_pred を直接消費。
|
| 59 |
+
"""
|
| 60 |
+
c_skip = torch.cos(t)
|
| 61 |
+
c_out = -torch.sin(t)
|
| 62 |
+
c_in = torch.cos(t) # 1/sqrt(tan(t)**2+1) = cos(t)
|
| 63 |
+
c_noise = t
|
| 64 |
+
return c_skip, c_out, c_in, c_noise
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ----- noise / re-noise (rectified flow native, sigma_data=1.0) -------------
|
| 68 |
+
|
| 69 |
+
def renoise_rf(x0: torch.Tensor, t_rf: torch.Tensor, noise: torch.Tensor) -> torch.Tensor:
|
| 70 |
+
"""rectified flow forward: x_t = (1-t)*x0 + t*noise。t_rf は ∈ [0,1]。"""
|
| 71 |
+
t_ = t_rf.view(-1, *([1] * (x0.dim() - 1)))
|
| 72 |
+
return (1.0 - t_) * x0 + t_ * noise
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def x0_from_velocity_rf(x_t: torch.Tensor, v: torch.Tensor, t_rf: torch.Tensor) -> torch.Tensor:
|
| 76 |
+
"""v = noise - x0 から x0 を復元: x0 = x_t - t * v"""
|
| 77 |
+
t_ = t_rf.view(-1, *([1] * (x_t.dim() - 1)))
|
| 78 |
+
return x_t - t_ * v
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ----- few-step student rollout --------------------------------------------
|
| 82 |
+
|
| 83 |
+
def backward_simulation(
|
| 84 |
+
student_v_fn: Callable[..., torch.Tensor],
|
| 85 |
+
noise: torch.Tensor,
|
| 86 |
+
n_steps: int,
|
| 87 |
+
cond_pos: torch.Tensor,
|
| 88 |
+
cond_neg: torch.Tensor | None,
|
| 89 |
+
cfg_scale: float = 1.0,
|
| 90 |
+
grad_last_only: bool = True,
|
| 91 |
+
) -> torch.Tensor:
|
| 92 |
+
"""t=1 (pure noise) から t=0 (clean) へ n_steps の Euler でロールアウト。
|
| 93 |
+
grad_last_only=True なら最終 1 step だけ grad を通し、それ以前は no_grad。
|
| 94 |
+
DMD2 paper 流派、memory efficient。
|
| 95 |
+
|
| 96 |
+
Returns: x_hat at t=0 (clean estimate)
|
| 97 |
+
"""
|
| 98 |
+
from .traj_loss import cfg_guided, _broadcast_t
|
| 99 |
+
|
| 100 |
+
B = noise.size(0)
|
| 101 |
+
device = noise.device
|
| 102 |
+
dtype = noise.dtype
|
| 103 |
+
|
| 104 |
+
# n_steps Euler points (t=1 → t=0)、両端含めて n+1 個
|
| 105 |
+
ts = torch.linspace(1.0, 0.0, n_steps + 1, device=device, dtype=torch.float32)
|
| 106 |
+
|
| 107 |
+
x = noise
|
| 108 |
+
for i in range(n_steps):
|
| 109 |
+
t_cur = ts[i]
|
| 110 |
+
t_next = ts[i + 1]
|
| 111 |
+
is_last = (i == n_steps - 1)
|
| 112 |
+
ctx = torch.enable_grad() if (is_last and grad_last_only) else \
|
| 113 |
+
(torch.no_grad() if grad_last_only else torch.enable_grad())
|
| 114 |
+
t_in = _broadcast_t(t_cur, B, device, dtype)
|
| 115 |
+
with ctx:
|
| 116 |
+
v = cfg_guided(student_v_fn, x, t_in, cond_pos, cond_neg, cfg_scale)
|
| 117 |
+
dt = (t_next - t_cur).to(device=device, dtype=dtype)
|
| 118 |
+
x = x + dt * v
|
| 119 |
+
return x
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ----- generator phase ------------------------------------------------------
|
| 123 |
+
|
| 124 |
+
def dmd2_generator_loss(
|
| 125 |
+
student_v_fn: Callable[..., torch.Tensor], # PEFT student (LoRA active)
|
| 126 |
+
teacher_v_fn: Callable[..., torch.Tensor], # frozen base, no LoRA
|
| 127 |
+
fake_score_v_fn: Callable[..., torch.Tensor], # PEFT fake_score (LoRA active, no grad here)
|
| 128 |
+
init_noise: torch.Tensor,
|
| 129 |
+
cond_pos: torch.Tensor,
|
| 130 |
+
cond_neg: torch.Tensor | None,
|
| 131 |
+
teacher_cfg: float = 3.0,
|
| 132 |
+
student_cfg: float = 1.0,
|
| 133 |
+
n_steps: int = 4,
|
| 134 |
+
shift: float = 5.0,
|
| 135 |
+
) -> tuple[torch.Tensor, dict]:
|
| 136 |
+
"""DMD2 generator step。
|
| 137 |
+
- student で few-step rollout、x_hat 推定 (grad は最終 step のみ)
|
| 138 |
+
- x_hat を t_D で re-noise
|
| 139 |
+
- teacher で CFG'd x0 推定
|
| 140 |
+
- fake_score で x0_fake 推定 (no_grad)
|
| 141 |
+
- DMD2 gradient trick で loss 計算
|
| 142 |
+
"""
|
| 143 |
+
from .traj_loss import _broadcast_t
|
| 144 |
+
|
| 145 |
+
B = init_noise.size(0)
|
| 146 |
+
device = init_noise.device
|
| 147 |
+
dtype = init_noise.dtype
|
| 148 |
+
|
| 149 |
+
# 1) few-step student rollout (grad only last)
|
| 150 |
+
x_hat = backward_simulation(
|
| 151 |
+
student_v_fn, init_noise, n_steps, cond_pos, cond_neg,
|
| 152 |
+
cfg_scale=student_cfg, grad_last_only=True,
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# 2) t_D for re-noise: shifted uniform on [0,1]
|
| 156 |
+
s = sample_shifted_uniform_s(B, shift=shift, device=device, dtype=torch.float32)
|
| 157 |
+
t_D = s # rectified flow time domain
|
| 158 |
+
D_eps = torch.randn_like(x_hat)
|
| 159 |
+
x_t = renoise_rf(x_hat, t_D, D_eps)
|
| 160 |
+
|
| 161 |
+
# 3) teacher CFG'd velocity → x0 estimate
|
| 162 |
+
with torch.no_grad():
|
| 163 |
+
t_in = t_D.to(dtype=dtype)
|
| 164 |
+
v_cond = teacher_v_fn(x_t, t_in, cond_pos)
|
| 165 |
+
if teacher_cfg > 1.0 and cond_neg is not None:
|
| 166 |
+
v_uncond = teacher_v_fn(x_t, t_in, cond_neg)
|
| 167 |
+
v_teacher = v_uncond + teacher_cfg * (v_cond - v_uncond)
|
| 168 |
+
else:
|
| 169 |
+
v_teacher = v_cond
|
| 170 |
+
x0_teacher = x0_from_velocity_rf(x_t, v_teacher, t_D)
|
| 171 |
+
|
| 172 |
+
# 4) fake_score velocity → x0_fake estimate (no_grad in gen phase)
|
| 173 |
+
v_fake = fake_score_v_fn(x_t, t_in, cond_pos)
|
| 174 |
+
x0_fake = x0_from_velocity_rf(x_t, v_fake, t_D)
|
| 175 |
+
|
| 176 |
+
# 5) DMD2 gradient trick
|
| 177 |
+
diff = x_hat - x0_teacher
|
| 178 |
+
# per-sample L1 normalization (sCM2 weight)
|
| 179 |
+
w = diff.abs().mean(dim=list(range(1, diff.dim())), keepdim=True).clamp(min=1e-5)
|
| 180 |
+
grad = (x0_fake.detach() - x0_teacher.detach()) / w
|
| 181 |
+
loss = ((x_hat - (x_hat - grad).detach()).float() ** 2).mean()
|
| 182 |
+
|
| 183 |
+
metrics = {
|
| 184 |
+
"l_dmd_gen": loss.detach(),
|
| 185 |
+
"t_D_mean": t_D.mean().detach(),
|
| 186 |
+
"x_hat_abs_mean": x_hat.detach().abs().mean(),
|
| 187 |
+
"teacher_abs_mean": x0_teacher.detach().abs().mean(),
|
| 188 |
+
"fake_abs_mean": x0_fake.detach().abs().mean(),
|
| 189 |
+
"w_mean": w.detach().mean(),
|
| 190 |
+
}
|
| 191 |
+
return loss, metrics
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# ----- critic (fake_score) phase -------------------------------------------
|
| 195 |
+
|
| 196 |
+
def dmd2_critic_loss(
|
| 197 |
+
student_v_fn: Callable[..., torch.Tensor],
|
| 198 |
+
fake_score_v_fn: Callable[..., torch.Tensor],
|
| 199 |
+
init_noise: torch.Tensor,
|
| 200 |
+
cond_pos: torch.Tensor,
|
| 201 |
+
cond_neg: torch.Tensor | None,
|
| 202 |
+
student_cfg: float = 1.0,
|
| 203 |
+
n_steps: int = 4,
|
| 204 |
+
shift: float = 5.0,
|
| 205 |
+
) -> tuple[torch.Tensor, dict]:
|
| 206 |
+
"""fake_score を student 分布の denoiser として学習。
|
| 207 |
+
weighted denoising loss with `1 / sin(t)**2` factor (上流の trigflow weighting に等価)。"""
|
| 208 |
+
from .traj_loss import _broadcast_t
|
| 209 |
+
|
| 210 |
+
B = init_noise.size(0)
|
| 211 |
+
device = init_noise.device
|
| 212 |
+
dtype = init_noise.dtype
|
| 213 |
+
|
| 214 |
+
# 1) student rollout (全部 no_grad: critic phase では student は固定扱い)
|
| 215 |
+
with torch.no_grad():
|
| 216 |
+
x_hat = backward_simulation(
|
| 217 |
+
student_v_fn, init_noise, n_steps, cond_pos, cond_neg,
|
| 218 |
+
cfg_scale=student_cfg, grad_last_only=False,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# 2) t_D で re-noise
|
| 222 |
+
s = sample_shifted_uniform_s(B, shift=shift, device=device, dtype=torch.float32)
|
| 223 |
+
t_D = s
|
| 224 |
+
D_eps = torch.randn_like(x_hat)
|
| 225 |
+
x_t = renoise_rf(x_hat, t_D, D_eps)
|
| 226 |
+
|
| 227 |
+
# 3) fake_score forward (grad on)
|
| 228 |
+
t_in = t_D.to(dtype=dtype)
|
| 229 |
+
v_fake = fake_score_v_fn(x_t, t_in, cond_pos)
|
| 230 |
+
x0_fake = x0_from_velocity_rf(x_t, v_fake, t_D)
|
| 231 |
+
|
| 232 |
+
# 4) weighted denoising loss: target = x_hat (= student output)
|
| 233 |
+
# weight = 1 / sin(arctan(s/(1-s)))**2、small s → big weight (high-noise emphasized)
|
| 234 |
+
t_trig = rf_s_to_trigflow_t(t_D)
|
| 235 |
+
w = 1.0 / torch.sin(t_trig).clamp(min=1e-3) ** 2
|
| 236 |
+
w = w.view(-1, *([1] * (x_hat.dim() - 1)))
|
| 237 |
+
loss = (w * (x_hat.detach() - x0_fake).float() ** 2).mean()
|
| 238 |
+
|
| 239 |
+
metrics = {
|
| 240 |
+
"l_dmd_critic": loss.detach(),
|
| 241 |
+
"t_D_mean": t_D.mean().detach(),
|
| 242 |
+
"w_mean": w.detach().mean(),
|
| 243 |
+
}
|
| 244 |
+
return loss, metrics
|
scripts/distill/dmd2_trainer.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DMD2 trainer for Anima (DMDR pattern + R3GAN + TSCD)
|
| 3 |
+
====================================================
|
| 4 |
+
|
| 5 |
+
DMDR paper の "2 モデル + LoRA on/off で real/fake 切替" 方式を踏襲。
|
| 6 |
+
ここに R3GAN discriminator と TSCD consistency loss を統合する。
|
| 7 |
+
|
| 8 |
+
Models:
|
| 9 |
+
gen_model : Anima DiT (full trainable, no LoRA)
|
| 10 |
+
guidance_model : Anima DiT (Q,V LoRA, scale 切替で real/fake)
|
| 11 |
+
discriminator : R3GAN projection D (text-conditional)
|
| 12 |
+
ema_gen : gen の EMA (TSCD target)
|
| 13 |
+
|
| 14 |
+
Update schedule per outer step:
|
| 15 |
+
- guidance × N_GUIDANCE (default 5)
|
| 16 |
+
- generator × 1
|
| 17 |
+
- discriminator × 1
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
import copy
|
| 21 |
+
import math
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
import torch.nn as nn
|
| 26 |
+
import torch.nn.functional as F
|
| 27 |
+
import peft
|
| 28 |
+
|
| 29 |
+
from .anima_loader import AnimaBundle
|
| 30 |
+
from .r3gan_disc import R3GANDiscriminator, d_loss_r3gan, g_loss_r3gan
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class DMD2Args:
|
| 35 |
+
# Training
|
| 36 |
+
batch_size: int = 4
|
| 37 |
+
lr_gen: float = 2e-5
|
| 38 |
+
lr_guidance: float = 2e-5
|
| 39 |
+
lr_disc: float = 2e-4
|
| 40 |
+
grad_clip: float = 1.0
|
| 41 |
+
|
| 42 |
+
# DMD2 / DMDR
|
| 43 |
+
guidance_updates: int = 5 # 1 outer step あたりの guidance update 回数
|
| 44 |
+
lora_rank: int = 32
|
| 45 |
+
lora_scale_f: float = 2.0 # fake-score (LoRA ON)
|
| 46 |
+
lora_scale_r: float = 0.75 # real-score start (cosine decay → 0)
|
| 47 |
+
cfg_r: float = 2.0 # real-score 側 CFG (Augmentation の Spear)
|
| 48 |
+
dynamic_decay_steps: int = 2000 # lora_scale_r を cosine で 0 にする step
|
| 49 |
+
|
| 50 |
+
# Timestep sampling
|
| 51 |
+
gui_alpha: float = 4.0 # Beta(α, β) for continuous t
|
| 52 |
+
gui_beta: float = 1.5
|
| 53 |
+
num_inference_steps: int = 8 # gen の discrete grid (Phase A=8, B=4, C=2)
|
| 54 |
+
|
| 55 |
+
# TSCD
|
| 56 |
+
tscd_weight: float = 0.5
|
| 57 |
+
ema_decay: float = 0.999
|
| 58 |
+
|
| 59 |
+
# R3GAN
|
| 60 |
+
adv_weight: float = 0.1
|
| 61 |
+
r3gan_gamma: float = 50.0
|
| 62 |
+
|
| 63 |
+
# CFG-Aug schedule
|
| 64 |
+
cfg_aug_cold_steps: int = 200 # 最初は cfg_r=0 で wait
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def attach_qv_lora(transformer: nn.Module, rank: int = 32) -> nn.Module:
|
| 68 |
+
"""attention Q/V projection に LoRA を attach。MiniTrainDIT の実 module 名を
|
| 69 |
+
inspection で見つける。"""
|
| 70 |
+
Q_KEYS = ("to_q", "q_proj", "wq", "q")
|
| 71 |
+
V_KEYS = ("to_v", "v_proj", "wv", "v")
|
| 72 |
+
target_modules = []
|
| 73 |
+
for name, module in transformer.named_modules():
|
| 74 |
+
if not isinstance(module, nn.Linear):
|
| 75 |
+
continue
|
| 76 |
+
leaf = name.rsplit(".", 1)[-1]
|
| 77 |
+
if leaf in Q_KEYS or leaf in V_KEYS:
|
| 78 |
+
target_modules.append(name)
|
| 79 |
+
|
| 80 |
+
if not target_modules:
|
| 81 |
+
# Fallback 1: 名前に 'attn' が入る Linear すべて
|
| 82 |
+
for name, module in transformer.named_modules():
|
| 83 |
+
if isinstance(module, nn.Linear) and (
|
| 84 |
+
"attn" in name.lower() or "attention" in name.lower()
|
| 85 |
+
):
|
| 86 |
+
target_modules.append(name)
|
| 87 |
+
|
| 88 |
+
if not target_modules:
|
| 89 |
+
print("[lora] WARN: no attention Q/V found, falling back to all-linear")
|
| 90 |
+
target_modules = "all-linear"
|
| 91 |
+
else:
|
| 92 |
+
print(f"[lora] target_modules={len(target_modules)} layers (Q/V or attn)")
|
| 93 |
+
|
| 94 |
+
cfg = peft.LoraConfig(
|
| 95 |
+
r=rank,
|
| 96 |
+
lora_alpha=rank,
|
| 97 |
+
lora_dropout=0.0,
|
| 98 |
+
bias="none",
|
| 99 |
+
target_modules=target_modules,
|
| 100 |
+
)
|
| 101 |
+
return peft.get_peft_model(transformer, cfg)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def attach_wide_lora(transformer: nn.Module, rank: int = 32) -> nn.Module:
|
| 105 |
+
"""全 nn.Linear に LoRA を attach (LLM adapter 内部は除外)。
|
| 106 |
+
Anima/Cosmos の AdaLN modulation・attention・MLP すべてを学習対象にする。
|
| 107 |
+
timestep 解釈を含む蒸留タスクには Q,V LoRA では不足、これが必要。"""
|
| 108 |
+
target_modules = []
|
| 109 |
+
for name, module in transformer.named_modules():
|
| 110 |
+
if not isinstance(module, nn.Linear):
|
| 111 |
+
continue
|
| 112 |
+
# LLM adapter (Qwen3→T5 bridge) は触らない (壊れやすい)
|
| 113 |
+
if "llm_adapter" in name:
|
| 114 |
+
continue
|
| 115 |
+
target_modules.append(name)
|
| 116 |
+
|
| 117 |
+
if not target_modules:
|
| 118 |
+
raise RuntimeError("No nn.Linear found in transformer for wide LoRA")
|
| 119 |
+
|
| 120 |
+
# 概要
|
| 121 |
+
cats = {"attn": 0, "adaln": 0, "mlp": 0, "other": 0}
|
| 122 |
+
for n in target_modules:
|
| 123 |
+
nl = n.lower()
|
| 124 |
+
if "attn" in nl or "attention" in nl:
|
| 125 |
+
cats["attn"] += 1
|
| 126 |
+
elif "adaln" in nl or "modulation" in nl:
|
| 127 |
+
cats["adaln"] += 1
|
| 128 |
+
elif "mlp" in nl or "ffn" in nl or "feed" in nl:
|
| 129 |
+
cats["mlp"] += 1
|
| 130 |
+
else:
|
| 131 |
+
cats["other"] += 1
|
| 132 |
+
print(f"[lora] wide target: total={len(target_modules)} "
|
| 133 |
+
f"(attn={cats['attn']}, adaln={cats['adaln']}, mlp={cats['mlp']}, other={cats['other']})")
|
| 134 |
+
|
| 135 |
+
cfg = peft.LoraConfig(
|
| 136 |
+
r=rank,
|
| 137 |
+
lora_alpha=rank,
|
| 138 |
+
lora_dropout=0.0,
|
| 139 |
+
bias="none",
|
| 140 |
+
target_modules=target_modules,
|
| 141 |
+
)
|
| 142 |
+
return peft.get_peft_model(transformer, cfg)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def set_lora_scale(model: nn.Module, scale: float) -> None:
|
| 146 |
+
"""PEFT LoRA の scaling を runtime で書き換え。real/fake 切替用。"""
|
| 147 |
+
for module in model.modules():
|
| 148 |
+
if hasattr(module, "scaling") and isinstance(module.scaling, dict):
|
| 149 |
+
for key in module.scaling:
|
| 150 |
+
# alpha/r = 1.0 を基準に scale 倍する
|
| 151 |
+
module.scaling[key] = scale
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def sample_continuous_t(
|
| 155 |
+
batch_size: int, alpha: float, beta: float, device: torch.device
|
| 156 |
+
) -> torch.Tensor:
|
| 157 |
+
"""logit-normal Beta(α, β) サンプリング。返り値 t ∈ (0, 1)"""
|
| 158 |
+
# Beta -> logit-normal 風の重み (DMDR の sample_continue 簡略版)
|
| 159 |
+
u = torch.rand(batch_size, device=device)
|
| 160 |
+
# Beta(α, β) inverse CDF を近似:正規 logit に変換するシンプル版
|
| 161 |
+
# ここでは log-normal で代用 (実装簡易のため)
|
| 162 |
+
t = torch.distributions.Beta(alpha, beta).sample((batch_size,)).to(device)
|
| 163 |
+
return t.clamp(1e-3, 1 - 1e-3)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def sample_discrete_t(
|
| 167 |
+
batch_size: int, num_steps: int, device: torch.device
|
| 168 |
+
) -> torch.Tensor:
|
| 169 |
+
"""generator 側: 離散 grid {1/N, 2/N, ..., (N-1)/N} から uniform サンプル。"""
|
| 170 |
+
grid = torch.linspace(1.0 / num_steps, 1.0 - 1.0 / num_steps, num_steps - 1, device=device)
|
| 171 |
+
idx = torch.randint(0, num_steps - 1, (batch_size,), device=device)
|
| 172 |
+
return grid[idx]
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def cosine_decay(step: int, total: int) -> float:
|
| 176 |
+
if step >= total:
|
| 177 |
+
return 0.0
|
| 178 |
+
return 0.5 * (1 + math.cos(math.pi * step / total))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class EMA:
|
| 182 |
+
"""生成器の EMA を別 model copy として保持。TSCD target として直接 forward 可能。
|
| 183 |
+
|
| 184 |
+
only_trainable=True (default) なら trainable param のみ EMA 更新する。
|
| 185 |
+
LoRA-only モードでは base は frozen なので大半が skip され、EMA cost が激減。"""
|
| 186 |
+
|
| 187 |
+
def __init__(self, model: nn.Module, decay: float = 0.999,
|
| 188 |
+
only_trainable: bool = True):
|
| 189 |
+
self.decay = decay
|
| 190 |
+
self.only_trainable = only_trainable
|
| 191 |
+
# 同 device/dtype の eval-only copy
|
| 192 |
+
self.ema_model = copy.deepcopy(model).eval()
|
| 193 |
+
for p in self.ema_model.parameters():
|
| 194 |
+
p.requires_grad = False
|
| 195 |
+
|
| 196 |
+
@torch.no_grad()
|
| 197 |
+
def update(self, model: nn.Module) -> None:
|
| 198 |
+
for ep, p in zip(self.ema_model.parameters(), model.parameters()):
|
| 199 |
+
if self.only_trainable and not p.requires_grad:
|
| 200 |
+
continue
|
| 201 |
+
ep.mul_(self.decay).add_(p.detach(), alpha=1 - self.decay)
|
| 202 |
+
# buffers (RoPE 等) は最新を採用
|
| 203 |
+
for eb, b in zip(self.ema_model.buffers(), model.buffers()):
|
| 204 |
+
eb.copy_(b.detach())
|
| 205 |
+
|
| 206 |
+
def __call__(self, **kwargs):
|
| 207 |
+
return self.ema_model(**kwargs)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# ============================================================================
|
| 211 |
+
# Trainer
|
| 212 |
+
# ============================================================================
|
| 213 |
+
|
| 214 |
+
class DMD2Trainer:
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
bundle: AnimaBundle,
|
| 218 |
+
gen_transformer: nn.Module,
|
| 219 |
+
guidance_transformer: nn.Module, # PEFT-wrapped (LoRA attached)
|
| 220 |
+
discriminator: R3GANDiscriminator,
|
| 221 |
+
args: DMD2Args,
|
| 222 |
+
):
|
| 223 |
+
self.bundle = bundle
|
| 224 |
+
self.gen = gen_transformer
|
| 225 |
+
self.guidance = guidance_transformer
|
| 226 |
+
self.disc = discriminator
|
| 227 |
+
self.args = args
|
| 228 |
+
self.device = bundle.device
|
| 229 |
+
self.step = 0
|
| 230 |
+
|
| 231 |
+
# Optimizers (gen も frozen 部分があるなら filter)
|
| 232 |
+
self.opt_gen = torch.optim.AdamW(
|
| 233 |
+
[p for p in self.gen.parameters() if p.requires_grad],
|
| 234 |
+
lr=args.lr_gen, betas=(0.9, 0.999),
|
| 235 |
+
weight_decay=0.01, eps=1e-8,
|
| 236 |
+
)
|
| 237 |
+
self.opt_guidance = torch.optim.AdamW(
|
| 238 |
+
[p for p in self.guidance.parameters() if p.requires_grad],
|
| 239 |
+
lr=args.lr_guidance, betas=(0.9, 0.999), weight_decay=0.0, eps=1e-8,
|
| 240 |
+
)
|
| 241 |
+
self.opt_disc = torch.optim.Adam(
|
| 242 |
+
self.disc.parameters(), lr=args.lr_disc, betas=(0.0, 0.99), eps=1e-8,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# EMA for TSCD
|
| 246 |
+
self.ema = EMA(self.gen, decay=args.ema_decay)
|
| 247 |
+
|
| 248 |
+
# -- helpers ------------------------------------------------------------
|
| 249 |
+
def _set_train(self, gen: bool, guidance: bool, disc: bool):
|
| 250 |
+
self.gen.train(gen); self.guidance.train(guidance); self.disc.train(disc)
|
| 251 |
+
|
| 252 |
+
def _t_continuous(self, B: int) -> torch.Tensor:
|
| 253 |
+
return sample_continuous_t(B, self.args.gui_alpha, self.args.gui_beta, self.device)
|
| 254 |
+
|
| 255 |
+
def _t_discrete(self, B: int) -> torch.Tensor:
|
| 256 |
+
return sample_discrete_t(B, self.args.num_inference_steps, self.device)
|
| 257 |
+
|
| 258 |
+
def _cfg_r_current(self) -> float:
|
| 259 |
+
"""cold start 中は cfg_r=0、それ以降は args.cfg_r。"""
|
| 260 |
+
return 0.0 if self.step < self.args.cfg_aug_cold_steps else self.args.cfg_r
|
| 261 |
+
|
| 262 |
+
def _lora_scale_r_current(self) -> float:
|
| 263 |
+
decay = cosine_decay(self.step, self.args.dynamic_decay_steps)
|
| 264 |
+
return self.args.lora_scale_r * decay
|
| 265 |
+
|
| 266 |
+
# -- guidance update ----------------------------------------------------
|
| 267 |
+
def step_guidance(self, real_latents: torch.Tensor, cond: torch.Tensor) -> dict:
|
| 268 |
+
"""fake-score (LoRA ON) を v-prediction MSE で更新"""
|
| 269 |
+
self._set_train(gen=False, guidance=True, disc=False)
|
| 270 |
+
set_lora_scale(self.guidance, self.args.lora_scale_f)
|
| 271 |
+
|
| 272 |
+
B = real_latents.size(0)
|
| 273 |
+
noise = torch.randn_like(real_latents)
|
| 274 |
+
t = self._t_continuous(B)
|
| 275 |
+
noisy = AnimaBundle.add_noise(real_latents, noise, t)
|
| 276 |
+
v_target = AnimaBundle.velocity_target(real_latents, noise)
|
| 277 |
+
|
| 278 |
+
v_pred = AnimaBundle.dit_forward(self.guidance, noisy, t, cond)
|
| 279 |
+
loss = F.mse_loss(v_pred, v_target)
|
| 280 |
+
loss.backward()
|
| 281 |
+
torch.nn.utils.clip_grad_norm_(
|
| 282 |
+
[p for p in self.guidance.parameters() if p.requires_grad],
|
| 283 |
+
self.args.grad_clip)
|
| 284 |
+
self.opt_guidance.step(); self.opt_guidance.zero_grad()
|
| 285 |
+
return {"l_guidance": loss.detach()}
|
| 286 |
+
|
| 287 |
+
# -- CFG forward (real-score side) -------------------------------------
|
| 288 |
+
def _real_score_forward(
|
| 289 |
+
self, noisy: torch.Tensor, t: torch.Tensor, cond: torch.Tensor, cfg: float,
|
| 290 |
+
) -> torch.Tensor:
|
| 291 |
+
"""LoRA OFF / decay 状態の guidance_model で real-side velocity を取得。
|
| 292 |
+
CFG > 1 のときは null condition と blend。"""
|
| 293 |
+
set_lora_scale(self.guidance, self._lora_scale_r_current())
|
| 294 |
+
v_cond = AnimaBundle.dit_forward(self.guidance, noisy, t, cond)
|
| 295 |
+
if cfg > 1.0:
|
| 296 |
+
null_cond = torch.zeros_like(cond)
|
| 297 |
+
v_uncond = AnimaBundle.dit_forward(self.guidance, noisy, t, null_cond)
|
| 298 |
+
v = v_uncond + cfg * (v_cond - v_uncond)
|
| 299 |
+
else:
|
| 300 |
+
v = v_cond
|
| 301 |
+
return v
|
| 302 |
+
|
| 303 |
+
# -- TSCD consistency loss ---------------------------------------------
|
| 304 |
+
def _tscd_loss(
|
| 305 |
+
self, x0_pred: torch.Tensor, cond: torch.Tensor, t: torch.Tensor
|
| 306 |
+
) -> torch.Tensor:
|
| 307 |
+
"""segment-wise consistency vs EMA gen。
|
| 308 |
+
TSCD 簡易版: 同じ x0 ground 周辺で noisy 化 → EMA で x0 推定 → MSE。"""
|
| 309 |
+
with torch.no_grad():
|
| 310 |
+
noise = torch.randn_like(x0_pred)
|
| 311 |
+
noisy = AnimaBundle.add_noise(x0_pred.detach(), noise, t)
|
| 312 |
+
v_ema = AnimaBundle.dit_forward(self.ema.ema_model, noisy, t, cond)
|
| 313 |
+
x0_ema = AnimaBundle.x0_from_velocity(noisy, v_ema, t)
|
| 314 |
+
return F.mse_loss(x0_pred, x0_ema.detach())
|
| 315 |
+
|
| 316 |
+
# -- generator update ---------------------------------------------------
|
| 317 |
+
def step_generator(self, real_latents: torch.Tensor, cond: torch.Tensor) -> dict:
|
| 318 |
+
self._set_train(gen=True, guidance=False, disc=False)
|
| 319 |
+
B = real_latents.size(0)
|
| 320 |
+
noise = torch.randn_like(real_latents)
|
| 321 |
+
t = self._t_discrete(B)
|
| 322 |
+
noisy = AnimaBundle.add_noise(real_latents, noise, t)
|
| 323 |
+
|
| 324 |
+
# Generator forward
|
| 325 |
+
v_gen = AnimaBundle.dit_forward(self.gen, noisy, t, cond)
|
| 326 |
+
x0_pred = AnimaBundle.x0_from_velocity(noisy, v_gen, t)
|
| 327 |
+
|
| 328 |
+
# === DMD gradient ===
|
| 329 |
+
with torch.no_grad():
|
| 330 |
+
cfg = self._cfg_r_current()
|
| 331 |
+
v_real = self._real_score_forward(noisy, t, cond, cfg=cfg)
|
| 332 |
+
set_lora_scale(self.guidance, self.args.lora_scale_f)
|
| 333 |
+
v_fake = AnimaBundle.dit_forward(self.guidance, noisy, t, cond)
|
| 334 |
+
x0_real = AnimaBundle.x0_from_velocity(noisy, v_real, t)
|
| 335 |
+
x0_fake = AnimaBundle.x0_from_velocity(noisy, v_fake, t)
|
| 336 |
+
p_real = x0_pred - x0_real
|
| 337 |
+
p_fake = x0_pred - x0_fake
|
| 338 |
+
denom = p_real.abs().mean(dim=list(range(1, p_real.dim())), keepdim=True) + 1e-8
|
| 339 |
+
grad = (p_real - p_fake) / denom
|
| 340 |
+
dmd_loss = 0.5 * F.mse_loss(x0_pred.float(), (x0_pred - grad).detach().float())
|
| 341 |
+
|
| 342 |
+
# === TSCD consistency ===
|
| 343 |
+
consist_loss = self._tscd_loss(x0_pred, cond, t)
|
| 344 |
+
|
| 345 |
+
# === R3GAN adversarial (generator side) — adv_weight>0 のときだけ ===
|
| 346 |
+
if self.args.adv_weight > 0:
|
| 347 |
+
d_fake = self.disc(x0_pred, cond)
|
| 348 |
+
with torch.no_grad():
|
| 349 |
+
d_real_for_g = self.disc(real_latents, cond)
|
| 350 |
+
adv_loss = g_loss_r3gan(d_real_for_g, d_fake)
|
| 351 |
+
else:
|
| 352 |
+
adv_loss = torch.zeros((), device=self.device)
|
| 353 |
+
|
| 354 |
+
loss = (
|
| 355 |
+
dmd_loss
|
| 356 |
+
+ self.args.tscd_weight * consist_loss
|
| 357 |
+
+ self.args.adv_weight * adv_loss
|
| 358 |
+
)
|
| 359 |
+
loss.backward()
|
| 360 |
+
torch.nn.utils.clip_grad_norm_(self.gen.parameters(), self.args.grad_clip)
|
| 361 |
+
self.opt_gen.step(); self.opt_gen.zero_grad()
|
| 362 |
+
|
| 363 |
+
self.ema.update(self.gen)
|
| 364 |
+
return {
|
| 365 |
+
"l_dmd": dmd_loss.detach(),
|
| 366 |
+
"l_consist": consist_loss.detach(),
|
| 367 |
+
"l_adv_g": adv_loss.detach(),
|
| 368 |
+
"loss_gen_total": loss.detach(),
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
# -- discriminator update ----------------------------------------------
|
| 372 |
+
def step_discriminator(self, real_latents: torch.Tensor, cond: torch.Tensor) -> dict:
|
| 373 |
+
self._set_train(gen=False, guidance=False, disc=True)
|
| 374 |
+
B = real_latents.size(0)
|
| 375 |
+
|
| 376 |
+
# 生成器で fake samples を作成 (no_grad)
|
| 377 |
+
with torch.no_grad():
|
| 378 |
+
noise = torch.randn_like(real_latents)
|
| 379 |
+
t_init = torch.ones(B, device=self.device)
|
| 380 |
+
v = AnimaBundle.dit_forward(self.gen, noise, t_init, cond)
|
| 381 |
+
fake_latents = AnimaBundle.x0_from_velocity(noise, v, t_init)
|
| 382 |
+
|
| 383 |
+
real_samples = real_latents.detach().clone().requires_grad_(True)
|
| 384 |
+
fake_samples = fake_latents.detach().clone().requires_grad_(True)
|
| 385 |
+
|
| 386 |
+
d_real = self.disc(real_samples, cond)
|
| 387 |
+
d_fake = self.disc(fake_samples, cond)
|
| 388 |
+
loss, metrics = d_loss_r3gan(
|
| 389 |
+
d_real, d_fake, real_samples, fake_samples, gamma=self.args.r3gan_gamma,
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
# GUARD: R1/R2 が異常に大きくなったら abort
|
| 393 |
+
# latent 空間 (16ch × 128×128 = 262k) では正常時でも R1/R2 ~ 10-100 になる。
|
| 394 |
+
# Phase A は 700 -> 700k で爆発。1000 を超えたら abort。
|
| 395 |
+
r1_val = float(metrics["d_r1"])
|
| 396 |
+
r2_val = float(metrics["d_r2"])
|
| 397 |
+
if r1_val > 1000.0 or r2_val > 1000.0:
|
| 398 |
+
raise RuntimeError(
|
| 399 |
+
f"R3GAN penalty exploded (r1={r1_val:.1f}, r2={r2_val:.1f}). "
|
| 400 |
+
f"Reduce r3gan_gamma (current={self.args.r3gan_gamma}). "
|
| 401 |
+
f"Aborting to prevent gen damage."
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
loss.backward()
|
| 405 |
+
torch.nn.utils.clip_grad_norm_(self.disc.parameters(), self.args.grad_clip)
|
| 406 |
+
self.opt_disc.step(); self.opt_disc.zero_grad()
|
| 407 |
+
metrics["l_disc"] = loss.detach()
|
| 408 |
+
return metrics
|
| 409 |
+
|
| 410 |
+
# -- main step ----------------------------------------------------------
|
| 411 |
+
def train_step(self, batch: dict) -> dict:
|
| 412 |
+
# 1) text encode + VAE encode (no grad)
|
| 413 |
+
with torch.no_grad():
|
| 414 |
+
cond = self.bundle.text_encode(batch["captions"])
|
| 415 |
+
real_latents = self.bundle.vae_encode(batch["pixels"].to(self.device))
|
| 416 |
+
|
| 417 |
+
# 2) guidance × N
|
| 418 |
+
log = {}
|
| 419 |
+
for _ in range(self.args.guidance_updates):
|
| 420 |
+
log.update(self.step_guidance(real_latents, cond))
|
| 421 |
+
|
| 422 |
+
# 3) generator × 1
|
| 423 |
+
log.update(self.step_generator(real_latents, cond))
|
| 424 |
+
|
| 425 |
+
# 4) discriminator × 1 (adv_weight=0 のときは skip して R3GAN を完全 disable)
|
| 426 |
+
if self.args.adv_weight > 0:
|
| 427 |
+
log.update(self.step_discriminator(real_latents, cond))
|
| 428 |
+
|
| 429 |
+
self.step += 1
|
| 430 |
+
return log
|
scripts/distill/dmdx_loss.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Anima DMDX (ADM = Adversarial Distribution Matching) distillation
|
| 3 |
+
=================================================================
|
| 4 |
+
Reference: arxiv 2507.18569v1
|
| 5 |
+
"Adversarial Distribution Matching for Diffusion Distillation"
|
| 6 |
+
|
| 7 |
+
DMD2 の grad trick (reverse KL) を、**学習可能 discriminator による hinge GAN
|
| 8 |
+
(TVD 最小化)** に置換した手法。本実装は image 用 ADM 単体 (ADP 省略)。
|
| 9 |
+
|
| 10 |
+
要点:
|
| 11 |
+
- student rollout は DMD2 と同じ (few-step、grad-last-only)
|
| 12 |
+
- sample t (cubic schedule、high-noise バイアス)
|
| 13 |
+
- x_hat を t に re-noise → x_t
|
| 14 |
+
- **teacher 1 step で t → t-Δt 進化** (DMDX の核、時刻情報を D に与える)
|
| 15 |
+
- D(x_{t-Δt}, t-Δt) で hinge generator loss
|
| 16 |
+
- real path は teacher_x0_cache の x0 を使用 (LADD と同じ流派)
|
| 17 |
+
- D backbone = teacher MiniTrainDIT (frozen) + spectral norm heads (LADD と共有)
|
| 18 |
+
|
| 19 |
+
vs DMD2:
|
| 20 |
+
- DMD2: real_score - fake_score (逆 KL gradient trick)
|
| 21 |
+
- DMDX: D(x_{t-Δt}_fake) ↑ / D(x_{t-Δt}_real) ↓ (hinge、対称 TVD)
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
from typing import Callable, List, Optional
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
|
| 29 |
+
from .dmd2_official_loss import backward_simulation, renoise_rf
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# ----- cubic time sampling -------------------------------------------------
|
| 33 |
+
|
| 34 |
+
def sample_cubic_t(B: int, device, dtype=torch.float32,
|
| 35 |
+
bias_high_noise: bool = True) -> torch.Tensor:
|
| 36 |
+
"""Cubic time schedule sampling.
|
| 37 |
+
bias_high_noise=True: u ~ U(0,1), t = 1 - u^3 → 多くは t > 0.5 (high noise)
|
| 38 |
+
paper の "高ノイズレベルへのバイアス" に対応。
|
| 39 |
+
"""
|
| 40 |
+
u = torch.rand(B, device=device, dtype=dtype)
|
| 41 |
+
if bias_high_noise:
|
| 42 |
+
return 1.0 - u ** 3
|
| 43 |
+
return u ** 3
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# ----- teacher Δt evolution -------------------------------------------------
|
| 47 |
+
|
| 48 |
+
def teacher_evolve_dt(
|
| 49 |
+
x_t: torch.Tensor, t: torch.Tensor, dt: torch.Tensor,
|
| 50 |
+
teacher_v_fn: Callable, cond_pos: torch.Tensor, cond_neg: torch.Tensor,
|
| 51 |
+
teacher_cfg: float = 4.5,
|
| 52 |
+
) -> torch.Tensor:
|
| 53 |
+
"""teacher CFG'd 1-step Euler で x_t (at time t) → x_{t-dt} (at time t-dt) へ進化。
|
| 54 |
+
rectified flow: x_{t-dt} = x_t - dt * v_teacher(x_t, t, cond)
|
| 55 |
+
"""
|
| 56 |
+
dtype = x_t.dtype
|
| 57 |
+
B = x_t.size(0)
|
| 58 |
+
with torch.no_grad():
|
| 59 |
+
t_in = t.to(dtype=dtype)
|
| 60 |
+
v_cond = teacher_v_fn(x_t, t_in, cond_pos)
|
| 61 |
+
if teacher_cfg > 1.0 and cond_neg is not None:
|
| 62 |
+
v_uncond = teacher_v_fn(x_t, t_in, cond_neg)
|
| 63 |
+
v = v_uncond + teacher_cfg * (v_cond - v_uncond)
|
| 64 |
+
else:
|
| 65 |
+
v = v_cond
|
| 66 |
+
dt_b = dt.view(-1, *([1] * (x_t.dim() - 1))).to(dtype=dtype)
|
| 67 |
+
x_next = x_t - dt_b * v
|
| 68 |
+
return x_next
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ----- ADM generator loss ---------------------------------------------------
|
| 72 |
+
|
| 73 |
+
def adm_generator_loss(
|
| 74 |
+
student_v_fn: Callable, teacher_v_fn: Callable, disc,
|
| 75 |
+
init_noise: torch.Tensor, cond_pos: torch.Tensor, cond_neg: torch.Tensor,
|
| 76 |
+
real_x0: Optional[torch.Tensor] = None,
|
| 77 |
+
teacher_cfg: float = 4.5, student_cfg: float = 1.0,
|
| 78 |
+
n_student_steps: int = 4, dt_ratio: float = 1.0 / 64,
|
| 79 |
+
recon_weight: float = 0.0,
|
| 80 |
+
) -> tuple[torch.Tensor, dict]:
|
| 81 |
+
"""ADM generator loss (hinge -D + optional recon anchor)。
|
| 82 |
+
1. student backward simulation で x_hat 推定 (grad-last-only)
|
| 83 |
+
2. cubic t sampling, t > dt_ratio
|
| 84 |
+
3. x_hat を t に re-noise → x_t_fake
|
| 85 |
+
4. teacher 1 step で x_{t-dt}_fake へ進化
|
| 86 |
+
5. D(x_{t-dt}_fake, t-dt) hinge GAN gen loss (mean(-D))
|
| 87 |
+
6. optional: recon_weight > 0 なら Smooth-L1 anchor (vs real_x0)
|
| 88 |
+
"""
|
| 89 |
+
B = init_noise.size(0)
|
| 90 |
+
device, dtype = init_noise.device, init_noise.dtype
|
| 91 |
+
|
| 92 |
+
# 1) student rollout (grad on last step only)
|
| 93 |
+
x_hat = backward_simulation(
|
| 94 |
+
student_v_fn, init_noise, n_student_steps, cond_pos, cond_neg,
|
| 95 |
+
cfg_scale=student_cfg, grad_last_only=True,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# 2) sample t (cubic, biased high-noise)
|
| 99 |
+
t = sample_cubic_t(B, device=device, dtype=torch.float32)
|
| 100 |
+
t = t.clamp(min=dt_ratio + 1e-3, max=1.0 - 1e-3)
|
| 101 |
+
|
| 102 |
+
# 3) re-noise x_hat to t
|
| 103 |
+
eps = torch.randn_like(x_hat)
|
| 104 |
+
x_t_fake = renoise_rf(x_hat, t, eps)
|
| 105 |
+
|
| 106 |
+
# 4) teacher Δt evolution
|
| 107 |
+
dt_vec = torch.full_like(t, dt_ratio)
|
| 108 |
+
t_next = t - dt_vec
|
| 109 |
+
x_next_fake = teacher_evolve_dt(
|
| 110 |
+
x_t_fake, t, dt_vec, teacher_v_fn, cond_pos, cond_neg, teacher_cfg=teacher_cfg,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# 5) discriminator at t-dt; gradient_to_input=True で student に grad が届く
|
| 114 |
+
t_next_in = t_next.to(dtype=dtype)
|
| 115 |
+
logits_fake = disc(x_next_fake, t_next_in, cond_pos, gradient_to_input=True)
|
| 116 |
+
# hinge generator: maximize D(fake) → minimize -D(fake)
|
| 117 |
+
loss_g = sum(-l.mean() for l in logits_fake) / max(len(logits_fake), 1)
|
| 118 |
+
|
| 119 |
+
metrics = {
|
| 120 |
+
"l_g_adv": loss_g.detach(),
|
| 121 |
+
"t_mean": t.mean().detach(),
|
| 122 |
+
"x_hat_abs_mean": x_hat.detach().abs().mean(),
|
| 123 |
+
"x_next_fake_abs_mean": x_next_fake.detach().abs().mean(),
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
# 6) optional recon anchor (LADD 流の mean collapse 防��)
|
| 127 |
+
if recon_weight > 0.0 and real_x0 is not None:
|
| 128 |
+
recon = F.smooth_l1_loss(x_hat.float(), real_x0.detach().float())
|
| 129 |
+
loss_g = loss_g + recon_weight * recon
|
| 130 |
+
metrics["l_g_recon"] = recon.detach()
|
| 131 |
+
|
| 132 |
+
return loss_g, metrics
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ----- ADM discriminator loss -----------------------------------------------
|
| 136 |
+
|
| 137 |
+
def adm_discriminator_loss(
|
| 138 |
+
student_v_fn: Callable, teacher_v_fn: Callable, disc,
|
| 139 |
+
init_noise: torch.Tensor, real_x0: torch.Tensor,
|
| 140 |
+
cond_pos: torch.Tensor, cond_neg: torch.Tensor,
|
| 141 |
+
teacher_cfg: float = 4.5, student_cfg: float = 1.0,
|
| 142 |
+
n_student_steps: int = 4, dt_ratio: float = 1.0 / 64,
|
| 143 |
+
) -> tuple[torch.Tensor, dict]:
|
| 144 |
+
"""ADM discriminator hinge loss。
|
| 145 |
+
Fake path: student rollout (no grad) → re-noise t → teacher t-dt
|
| 146 |
+
Real path: real_x0 → re-noise t → teacher t-dt
|
| 147 |
+
Hinge: relu(1 - D(real)) + relu(1 + D(fake))
|
| 148 |
+
"""
|
| 149 |
+
B = init_noise.size(0)
|
| 150 |
+
device, dtype = init_noise.device, init_noise.dtype
|
| 151 |
+
|
| 152 |
+
# Fake path (no student grad in D phase)
|
| 153 |
+
with torch.no_grad():
|
| 154 |
+
x_hat = backward_simulation(
|
| 155 |
+
student_v_fn, init_noise, n_student_steps, cond_pos, cond_neg,
|
| 156 |
+
cfg_scale=student_cfg, grad_last_only=False,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
# sample t (cubic)
|
| 160 |
+
t = sample_cubic_t(B, device=device, dtype=torch.float32)
|
| 161 |
+
t = t.clamp(min=dt_ratio + 1e-3, max=1.0 - 1e-3)
|
| 162 |
+
|
| 163 |
+
# re-noise (fake / real)
|
| 164 |
+
eps_f = torch.randn_like(x_hat)
|
| 165 |
+
eps_r = torch.randn_like(real_x0)
|
| 166 |
+
x_t_fake = renoise_rf(x_hat, t, eps_f)
|
| 167 |
+
x_t_real = renoise_rf(real_x0, t, eps_r)
|
| 168 |
+
|
| 169 |
+
# Δt evolution (both)
|
| 170 |
+
dt_vec = torch.full_like(t, dt_ratio)
|
| 171 |
+
t_next = t - dt_vec
|
| 172 |
+
x_next_fake = teacher_evolve_dt(
|
| 173 |
+
x_t_fake, t, dt_vec, teacher_v_fn, cond_pos, cond_neg, teacher_cfg=teacher_cfg,
|
| 174 |
+
)
|
| 175 |
+
x_next_real = teacher_evolve_dt(
|
| 176 |
+
x_t_real, t, dt_vec, teacher_v_fn, cond_pos, cond_neg, teacher_cfg=teacher_cfg,
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
# D at t-dt (heads only, x detached、メモリ節約)
|
| 180 |
+
t_next_in = t_next.to(dtype=dtype)
|
| 181 |
+
logits_fake = disc(x_next_fake.detach(), t_next_in, cond_pos, gradient_to_input=False)
|
| 182 |
+
logits_real = disc(x_next_real.detach(), t_next_in, cond_pos, gradient_to_input=False)
|
| 183 |
+
|
| 184 |
+
# hinge: real → +1、fake → -1
|
| 185 |
+
loss_real = sum(F.relu(1.0 - l).mean() for l in logits_real) / max(len(logits_real), 1)
|
| 186 |
+
loss_fake = sum(F.relu(1.0 + l).mean() for l in logits_fake) / max(len(logits_fake), 1)
|
| 187 |
+
loss_d = loss_real + loss_fake
|
| 188 |
+
|
| 189 |
+
metrics = {
|
| 190 |
+
"l_d_real": loss_real.detach(),
|
| 191 |
+
"l_d_fake": loss_fake.detach(),
|
| 192 |
+
"l_d_total": loss_d.detach(),
|
| 193 |
+
"t_mean": t.mean().detach(),
|
| 194 |
+
}
|
| 195 |
+
return loss_d, metrics
|
scripts/distill/hps_reward.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HPSv2 (Human Preference Score v2) を differentiable reward として使うラッパ。
|
| 3 |
+
|
| 4 |
+
公式 hpsv2 package は score を file path 経由でしか返さないので、
|
| 5 |
+
内部の OpenCLIP H/14 + custom checkpoint を直接ロードして grad-through 化する。
|
| 6 |
+
|
| 7 |
+
Repo: https://github.com/tgxs002/HPSv2
|
| 8 |
+
Weights: HPS_v2_compressed.pt (1.97 GB、HF Space xswu/HPSv2)
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
import os
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class HPSv2Reward(nn.Module):
|
| 20 |
+
"""HPSv2 differentiable reward (higher is better)。
|
| 21 |
+
|
| 22 |
+
forward(images, captions) → (B,) reward
|
| 23 |
+
images: (B, 3, H, W) in [-1, 1] (CLIP preprocessing は内部で実施)
|
| 24 |
+
captions: list[str]
|
| 25 |
+
"""
|
| 26 |
+
def __init__(self, weights_path: str, device="cuda", dtype=torch.float32):
|
| 27 |
+
super().__init__()
|
| 28 |
+
import open_clip
|
| 29 |
+
self.device = device
|
| 30 |
+
self.dtype = dtype
|
| 31 |
+
# OpenCLIP ViT-H/14 を laion2b プリトレで初期化
|
| 32 |
+
self.model, _, self.preprocess = open_clip.create_model_and_transforms(
|
| 33 |
+
"ViT-H-14", pretrained="laion2b_s32b_b79k", device=device,
|
| 34 |
+
)
|
| 35 |
+
self.tokenizer = open_clip.get_tokenizer("ViT-H-14")
|
| 36 |
+
# HPSv2 の preference-finetuned weights を上書き
|
| 37 |
+
if weights_path and os.path.exists(weights_path):
|
| 38 |
+
sd = torch.load(weights_path, map_location=device, weights_only=False)
|
| 39 |
+
if "state_dict" in sd:
|
| 40 |
+
sd = sd["state_dict"]
|
| 41 |
+
# remove 'module.' prefix if present
|
| 42 |
+
sd = {k.replace("module.", ""): v for k, v in sd.items()}
|
| 43 |
+
missing, unexpected = self.model.load_state_dict(sd, strict=False)
|
| 44 |
+
print(f"[hpsv2] loaded weights: missing={len(missing)} unexpected={len(unexpected)}")
|
| 45 |
+
else:
|
| 46 |
+
print(f"[hpsv2] WARNING: weights_path '{weights_path}' not found, using OpenCLIP baseline only")
|
| 47 |
+
|
| 48 |
+
# freeze
|
| 49 |
+
for p in self.model.parameters():
|
| 50 |
+
p.requires_grad = False
|
| 51 |
+
self.model.eval()
|
| 52 |
+
|
| 53 |
+
# CLIP normalization 定数
|
| 54 |
+
self.register_buffer("mean", torch.tensor([0.48145466, 0.4578275, 0.40821073]).view(1, 3, 1, 1))
|
| 55 |
+
self.register_buffer("std", torch.tensor([0.26862954, 0.26130258, 0.27577711]).view(1, 3, 1, 1))
|
| 56 |
+
# device / dtype を明示的に移動 (register_buffer は default CPU なので必須)
|
| 57 |
+
self.to(device=device, dtype=dtype)
|
| 58 |
+
|
| 59 |
+
def preprocess_images(self, images: torch.Tensor) -> torch.Tensor:
|
| 60 |
+
"""images (B, 3, H, W) in [-1, 1] → 224 bicubic + CLIP normalize、grad-through。"""
|
| 61 |
+
# [-1, 1] → [0, 1]
|
| 62 |
+
x = (images + 1.0) / 2.0
|
| 63 |
+
x = x.clamp(0.0, 1.0)
|
| 64 |
+
# resize to 224
|
| 65 |
+
x = F.interpolate(x, size=(224, 224), mode="bicubic", align_corners=False, antialias=True)
|
| 66 |
+
# normalize
|
| 67 |
+
x = (x - self.mean) / self.std
|
| 68 |
+
return x
|
| 69 |
+
|
| 70 |
+
def score(self, images: torch.Tensor, captions: list[str]) -> torch.Tensor:
|
| 71 |
+
"""grad-through HPSv2 score。"""
|
| 72 |
+
img = self.preprocess_images(images.to(self.device, dtype=self.dtype))
|
| 73 |
+
with torch.no_grad():
|
| 74 |
+
tokens = self.tokenizer(captions).to(self.device)
|
| 75 |
+
# text features (no grad)
|
| 76 |
+
with torch.no_grad():
|
| 77 |
+
text_feats = self.model.encode_text(tokens)
|
| 78 |
+
text_feats = text_feats / text_feats.norm(dim=-1, keepdim=True)
|
| 79 |
+
# image features (grad on)
|
| 80 |
+
image_feats = self.model.encode_image(img)
|
| 81 |
+
image_feats = image_feats / image_feats.norm(dim=-1, keepdim=True)
|
| 82 |
+
# cosine similarity scaled (HPSv2 uses temperature)
|
| 83 |
+
logit_scale = self.model.logit_scale.exp().detach()
|
| 84 |
+
scores = (image_feats * text_feats).sum(dim=-1) * logit_scale
|
| 85 |
+
return scores
|
scripts/distill/pcm_scheduler.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PCM (Phased Consistency Model) 用 Euler solver + phase snap
|
| 3 |
+
=========================================================
|
| 4 |
+
|
| 5 |
+
Reference: G-U-N/Phased-Consistency-Model
|
| 6 |
+
code/text_to_image_sd3/pcm_fm_deterministic_scheduler.py
|
| 7 |
+
|
| 8 |
+
Anima は rectified flow なので SD3 用 PCM (FlowMatchEulerDiscrete) と math 完全一致。
|
| 9 |
+
sigma == t、x_t = sigma*noise + (1-sigma)*x0、v = noise - x0、x_prev = x_t + dt*v
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class PCMEulerSolver:
|
| 20 |
+
"""num_euler_timesteps の grid (1.0 → 0.0) と、K phase の境界 index を保持。
|
| 21 |
+
|
| 22 |
+
Attributes:
|
| 23 |
+
sigmas: (N+1,) tensor、t=1 → 0 (= sigma_max → 0)
|
| 24 |
+
sigmas_prev: (N,) tensor、各 sigmas[i+1]
|
| 25 |
+
phase_ends: (K,) int tensor、各 phase の **終端 sigmas index**
|
| 26 |
+
"""
|
| 27 |
+
sigmas: torch.Tensor # (N+1,)
|
| 28 |
+
sigmas_prev: torch.Tensor # (N,)
|
| 29 |
+
phase_ends: torch.Tensor # (K,) ints in [1, N]
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def num_steps(self) -> int:
|
| 33 |
+
return len(self.sigmas_prev)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def make_pcm_solver(
|
| 37 |
+
num_euler_timesteps: int = 50,
|
| 38 |
+
num_phases: int = 4,
|
| 39 |
+
sigma_shift: float = 3.0,
|
| 40 |
+
device=None, dtype=torch.float32,
|
| 41 |
+
) -> PCMEulerSolver:
|
| 42 |
+
"""linear schedule + sigmoid shift。K 個に等分割。
|
| 43 |
+
phase_ends は各 phase の終端 sigma の **index** (= 次 phase の 開始 sigma index と一致)。"""
|
| 44 |
+
from .traj_scheduler import sigmoid_shift
|
| 45 |
+
ts = torch.linspace(1.0, 0.0, num_euler_timesteps + 1, dtype=torch.float64)
|
| 46 |
+
ts = sigmoid_shift(ts, sigma_shift)
|
| 47 |
+
sigmas = ts.to(device=device, dtype=dtype)
|
| 48 |
+
sigmas_prev = sigmas[1:]
|
| 49 |
+
# phase boundaries: np.linspace(0, N, K, endpoint=False) → starts
|
| 50 |
+
# 終端は次 phase の開始 - 1。最後の phase の終端は N (clean)。
|
| 51 |
+
starts = np.linspace(0, num_euler_timesteps, num_phases, endpoint=False, dtype=int)
|
| 52 |
+
ends = list(starts[1:]) + [num_euler_timesteps] # K 個
|
| 53 |
+
phase_ends = torch.tensor(ends, dtype=torch.long, device=device)
|
| 54 |
+
return PCMEulerSolver(sigmas=sigmas, sigmas_prev=sigmas_prev, phase_ends=phase_ends)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def euler_step_pcm(
|
| 58 |
+
x: torch.Tensor, v: torch.Tensor,
|
| 59 |
+
sigma_cur: torch.Tensor, sigma_next: torch.Tensor,
|
| 60 |
+
) -> torch.Tensor:
|
| 61 |
+
"""rectified-flow Euler 1 step: x_next = x + (sigma_next - sigma_cur) * v
|
| 62 |
+
(sigma 減少方向、v は noise - x0)"""
|
| 63 |
+
dt = (sigma_next - sigma_cur).view(-1, *([1] * (x.dim() - 1)))
|
| 64 |
+
return x + dt * v
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def euler_multiphase(
|
| 68 |
+
x_t: torch.Tensor, v: torch.Tensor, index: torch.Tensor,
|
| 69 |
+
solver: PCMEulerSolver,
|
| 70 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 71 |
+
"""index で指定した時点 sigma から、その所属 phase の終端 sigma まで 1-step Euler。
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
x_t: (B, ...)
|
| 75 |
+
v: (B, ...) velocity prediction
|
| 76 |
+
index: (B,) long、各サンプルの現在の sigmas grid index
|
| 77 |
+
solver: PCMEulerSolver
|
| 78 |
+
Returns:
|
| 79 |
+
x_at_phase_end: (B, ...)
|
| 80 |
+
sigma_at_phase_end: (B,)
|
| 81 |
+
"""
|
| 82 |
+
B = x_t.size(0)
|
| 83 |
+
device = x_t.device
|
| 84 |
+
# 各 sample が属する phase の終端 index を引く
|
| 85 |
+
phase_ends = solver.phase_ends # (K,)
|
| 86 |
+
# index → phase index: searchsorted で「index 以上の最初の phase_end」
|
| 87 |
+
# ただし PCM 流は「index <= phase_end - 1 を満たす最小 phase_end」
|
| 88 |
+
# ここでは現在 sigmas[index] からその phase の sigmas[phase_end] まで進める
|
| 89 |
+
# = sigma_cur = sigmas[index], sigma_target = sigmas[phase_end]
|
| 90 |
+
idx_np = index.detach().cpu().numpy()
|
| 91 |
+
end_np = phase_ends.detach().cpu().numpy()
|
| 92 |
+
target_end = []
|
| 93 |
+
for ix in idx_np:
|
| 94 |
+
# 最初に phase_end >= ix+1 になる phase を選ぶ (= 現 phase の終端)
|
| 95 |
+
end = end_np[end_np > ix][0] if (end_np > ix).any() else end_np[-1]
|
| 96 |
+
target_end.append(int(end))
|
| 97 |
+
target_end_t = torch.tensor(target_end, dtype=torch.long, device=device)
|
| 98 |
+
|
| 99 |
+
sigma_cur = solver.sigmas[index] # (B,)
|
| 100 |
+
sigma_target = solver.sigmas[target_end_t] # (B,)
|
| 101 |
+
x_next = euler_step_pcm(x_t, v, sigma_cur, sigma_target)
|
| 102 |
+
return x_next, sigma_target
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def pseudo_huber(diff: torch.Tensor, c: float = 1e-3) -> torch.Tensor:
|
| 106 |
+
"""sqrt(diff^2 + c^2) - c。PCM 公式の robust loss。"""
|
| 107 |
+
return torch.sqrt(diff.float() ** 2 + c * c) - c
|
scripts/distill/precompute_teacher_x0.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Precompute teacher x0 cache for AMD LADD training.
|
| 4 |
+
|
| 5 |
+
For each caption in the dataset:
|
| 6 |
+
1. Sample fresh noise
|
| 7 |
+
2. Run teacher (= base Anima, no LoRA) for N steps with CFG=W
|
| 8 |
+
3. Save final x0 (decoded latent stays in latent space) as .pt
|
| 9 |
+
4. Save caption embedding too (avoid re-encoding during training)
|
| 10 |
+
|
| 11 |
+
Output structure:
|
| 12 |
+
/dataset/teacher_x0_cache/
|
| 13 |
+
metadata.json [{caption, x0_path, emb_path}, ...]
|
| 14 |
+
x0/0001.pt (B=1, 16, 1, H_lat, W_lat) bf16
|
| 15 |
+
emb/0001.pt (B=1, 512, 1024) bf16
|
| 16 |
+
|
| 17 |
+
LADD train ループはこの cache を読むだけで、teacher を都度 forward しないので
|
| 18 |
+
1 step あたりの時間を半分以下に。
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
import argparse
|
| 22 |
+
import json
|
| 23 |
+
import sys
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
from torch.utils.data import DataLoader
|
| 28 |
+
|
| 29 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 30 |
+
from distill.anima_loader import build_anima, AnimaBundle
|
| 31 |
+
from distill.train_traj import TextOnlyDataset, text_collate
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@torch.no_grad()
|
| 35 |
+
def teacher_rollout(
|
| 36 |
+
bundle: AnimaBundle,
|
| 37 |
+
cond_pos: torch.Tensor,
|
| 38 |
+
cond_neg: torch.Tensor,
|
| 39 |
+
H_lat: int, W_lat: int,
|
| 40 |
+
num_steps: int = 20,
|
| 41 |
+
cfg_scale: float = 4.5,
|
| 42 |
+
sigma_shift: float = 3.0,
|
| 43 |
+
device=None, dtype=torch.bfloat16,
|
| 44 |
+
init_noise: torch.Tensor | None = None,
|
| 45 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 46 |
+
"""teacher (= base Anima、frozen) を N step Euler で回して x0 を返す。
|
| 47 |
+
sigma_shift は Anima 公式 workflow 推奨 3.0。
|
| 48 |
+
init_noise を渡せば外で seed 管理可能。返り値は (x0_final, noise_used)。"""
|
| 49 |
+
from distill.traj_scheduler import make_schedule
|
| 50 |
+
sched = make_schedule(num_steps, sigma_shift, device=device, dtype=torch.float32)
|
| 51 |
+
ts = sched.timesteps # (N+1,) t=1 → 0
|
| 52 |
+
B = cond_pos.size(0)
|
| 53 |
+
if init_noise is None:
|
| 54 |
+
init_noise = torch.randn(B, 16, 1, H_lat, W_lat, device=device, dtype=dtype)
|
| 55 |
+
x = init_noise.clone()
|
| 56 |
+
for i in range(num_steps):
|
| 57 |
+
t_cur = ts[i].expand(B).to(dtype=dtype)
|
| 58 |
+
v_cond = bundle.velocity(x, t_cur, cond_pos)
|
| 59 |
+
if cfg_scale > 1.0:
|
| 60 |
+
v_uncond = bundle.velocity(x, t_cur, cond_neg)
|
| 61 |
+
v = v_uncond + cfg_scale * (v_cond - v_uncond)
|
| 62 |
+
else:
|
| 63 |
+
v = v_cond
|
| 64 |
+
dt = (ts[i + 1] - ts[i]).to(device=device, dtype=dtype)
|
| 65 |
+
x = x + dt * v
|
| 66 |
+
return x, init_noise
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def main():
|
| 70 |
+
ap = argparse.ArgumentParser()
|
| 71 |
+
ap.add_argument("--dataset", required=True, type=str, help="caption dir")
|
| 72 |
+
ap.add_argument("--out", required=True, type=str, help="cache output dir")
|
| 73 |
+
ap.add_argument("--num-steps", type=int, default=20)
|
| 74 |
+
ap.add_argument("--cfg-scale", type=float, default=4.5)
|
| 75 |
+
ap.add_argument("--sigma-shift", type=float, default=3.0)
|
| 76 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 77 |
+
ap.add_argument("--max-samples", type=int, default=-1, help="-1 = all")
|
| 78 |
+
ap.add_argument("--neg-prompt", default="")
|
| 79 |
+
ap.add_argument("--start-from", type=int, default=0, help="resume index")
|
| 80 |
+
ap.add_argument("--save-noise", action="store_true",
|
| 81 |
+
help="initial noise も保存 (Reflow 用 (noise, x0) pair に必須)")
|
| 82 |
+
ap.add_argument("--metadata-out", default="",
|
| 83 |
+
help="metadata 出力先 (省略時は <out>/metadata.json)。"
|
| 84 |
+
"並列実行時に worker ごと別ファイルにして race を回避するため。")
|
| 85 |
+
args = ap.parse_args()
|
| 86 |
+
|
| 87 |
+
device = torch.device("cuda")
|
| 88 |
+
dtype = torch.bfloat16
|
| 89 |
+
|
| 90 |
+
out_dir = Path(args.out)
|
| 91 |
+
(out_dir / "x0").mkdir(parents=True, exist_ok=True)
|
| 92 |
+
(out_dir / "emb").mkdir(parents=True, exist_ok=True)
|
| 93 |
+
if args.save_noise:
|
| 94 |
+
(out_dir / "noise").mkdir(parents=True, exist_ok=True)
|
| 95 |
+
|
| 96 |
+
print("[load] Anima bundle (teacher only)")
|
| 97 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 98 |
+
|
| 99 |
+
print(f"[data] {args.dataset}")
|
| 100 |
+
ds = TextOnlyDataset(args.dataset)
|
| 101 |
+
N = len(ds) if args.max_samples < 0 else min(len(ds), args.max_samples)
|
| 102 |
+
print(f" precomputing {N - args.start_from} samples (resolution={args.resolution})")
|
| 103 |
+
|
| 104 |
+
H_lat = args.resolution // 8
|
| 105 |
+
W_lat = args.resolution // 8
|
| 106 |
+
|
| 107 |
+
with torch.no_grad():
|
| 108 |
+
cond_neg = bundle.text_encode([args.neg_prompt or ""])
|
| 109 |
+
|
| 110 |
+
metadata = []
|
| 111 |
+
metadata_path = Path(args.metadata_out) if args.metadata_out else out_dir / "metadata.json"
|
| 112 |
+
metadata_path.parent.mkdir(parents=True, exist_ok=True)
|
| 113 |
+
if metadata_path.exists() and args.start_from > 0:
|
| 114 |
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
| 115 |
+
|
| 116 |
+
import time
|
| 117 |
+
t_start = time.time()
|
| 118 |
+
for i in range(args.start_from, N):
|
| 119 |
+
caption = ds[i]
|
| 120 |
+
with torch.no_grad():
|
| 121 |
+
cond_pos = bundle.text_encode([caption])
|
| 122 |
+
x0, noise = teacher_rollout(
|
| 123 |
+
bundle, cond_pos, cond_neg, H_lat, W_lat,
|
| 124 |
+
num_steps=args.num_steps, cfg_scale=args.cfg_scale,
|
| 125 |
+
sigma_shift=args.sigma_shift, device=device, dtype=dtype,
|
| 126 |
+
)
|
| 127 |
+
x0_p = out_dir / "x0" / f"{i:05d}.pt"
|
| 128 |
+
emb_p = out_dir / "emb" / f"{i:05d}.pt"
|
| 129 |
+
torch.save(x0.cpu(), x0_p)
|
| 130 |
+
torch.save(cond_pos.cpu(), emb_p)
|
| 131 |
+
entry = {"caption": caption, "x0_path": str(x0_p), "emb_path": str(emb_p)}
|
| 132 |
+
if args.save_noise:
|
| 133 |
+
noise_p = out_dir / "noise" / f"{i:05d}.pt"
|
| 134 |
+
torch.save(noise.cpu(), noise_p)
|
| 135 |
+
entry["noise_path"] = str(noise_p)
|
| 136 |
+
metadata.append(entry)
|
| 137 |
+
|
| 138 |
+
if i % 50 == 0 or i == N - 1:
|
| 139 |
+
metadata_path.write_text(json.dumps(metadata, ensure_ascii=False), encoding="utf-8")
|
| 140 |
+
elapsed = time.time() - t_start
|
| 141 |
+
rate = (i - args.start_from + 1) / max(elapsed, 1e-3)
|
| 142 |
+
eta = (N - i - 1) / max(rate, 1e-3)
|
| 143 |
+
print(f"[precompute {i}/{N}] rate={rate:.2f}/s eta={eta/60:.1f}min", flush=True)
|
| 144 |
+
try:
|
| 145 |
+
import modal
|
| 146 |
+
modal.Volume.from_name("anima-dataset").commit()
|
| 147 |
+
except Exception:
|
| 148 |
+
pass
|
| 149 |
+
|
| 150 |
+
metadata_path.write_text(json.dumps(metadata, ensure_ascii=False), encoding="utf-8")
|
| 151 |
+
print(f"[done] {len(metadata)} samples cached to {out_dir}")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
main()
|
scripts/distill/r3gan_disc.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
R3GAN (NeurIPS 2024) discriminator + losses (RpGAN + R1 + R2)。
|
| 3 |
+
|
| 4 |
+
Anima 用に latent-space で動かす:
|
| 5 |
+
- 入力: VAE latents (B, 16, T, H_lat, W_lat) — T=1 (静止画)
|
| 6 |
+
- 条件: Qwen3+LLMAdapter の crossattn_emb を平均プールして projection D
|
| 7 |
+
- 出力: scalar critic value per sample
|
| 8 |
+
|
| 9 |
+
Normalization 不使用 (R3GAN の "modernized GAN" の要)、MSR init、
|
| 10 |
+
activation gain で variance を解析的に維持。
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
import math
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
import torch.nn as nn
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def msr_init(weight: torch.Tensor, gain: float = 1.0) -> None:
|
| 22 |
+
"""MSR (He) initialization, fan-in 基準。"""
|
| 23 |
+
fan_in = weight.shape[1] * weight[0][0].numel()
|
| 24 |
+
std = gain / math.sqrt(fan_in)
|
| 25 |
+
with torch.no_grad():
|
| 26 |
+
weight.normal_(0, std)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class BiasedActivation(nn.Module):
|
| 30 |
+
"""Leaky ReLU + 学習可能 bias。R3GAN の活性化代替。"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, channels: int, negative_slope: float = 0.2):
|
| 33 |
+
super().__init__()
|
| 34 |
+
self.bias = nn.Parameter(torch.zeros(channels))
|
| 35 |
+
self.negative_slope = negative_slope
|
| 36 |
+
|
| 37 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 38 |
+
return F.leaky_relu(x + self.bias.view(1, -1, *([1] * (x.dim() - 2))),
|
| 39 |
+
self.negative_slope, inplace=True)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class ResidualBlock(nn.Module):
|
| 43 |
+
"""ConvNeXt-style: 1x1 expand -> grouped depthwise -> 1x1 project, no norm."""
|
| 44 |
+
|
| 45 |
+
def __init__(self, channels: int, expansion: int = 2, cardinality: int = 1, kernel: int = 3):
|
| 46 |
+
super().__init__()
|
| 47 |
+
hidden = channels * expansion
|
| 48 |
+
groups = max(1, hidden // cardinality) # depthwise-ish grouping
|
| 49 |
+
|
| 50 |
+
self.expand = nn.Conv2d(channels, hidden, kernel_size=1, bias=False)
|
| 51 |
+
self.dw = nn.Conv2d(hidden, hidden, kernel_size=kernel, padding=kernel // 2,
|
| 52 |
+
groups=groups, bias=False)
|
| 53 |
+
self.project = nn.Conv2d(hidden, channels, kernel_size=1, bias=False)
|
| 54 |
+
|
| 55 |
+
self.act = BiasedActivation(hidden)
|
| 56 |
+
|
| 57 |
+
# MSR init, project は ActivationGain=0 で残差ゼロ初期化(安定)
|
| 58 |
+
msr_init(self.expand.weight, gain=1.0)
|
| 59 |
+
msr_init(self.dw.weight, gain=1.0)
|
| 60 |
+
nn.init.zeros_(self.project.weight)
|
| 61 |
+
|
| 62 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 63 |
+
h = self.expand(x)
|
| 64 |
+
h = self.act(h)
|
| 65 |
+
h = self.dw(h)
|
| 66 |
+
h = self.act(h)
|
| 67 |
+
h = self.project(h)
|
| 68 |
+
return x + h
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class DownsampleLayer(nn.Module):
|
| 72 |
+
"""[1,2,1] bilinear / anti-aliased downsample by factor 2."""
|
| 73 |
+
|
| 74 |
+
def __init__(self):
|
| 75 |
+
super().__init__()
|
| 76 |
+
kernel = torch.tensor([1.0, 2.0, 1.0]) / 4.0
|
| 77 |
+
kernel = kernel[:, None] * kernel[None, :] # (3, 3)
|
| 78 |
+
self.register_buffer("kernel", kernel[None, None].repeat(1, 1, 1, 1))
|
| 79 |
+
|
| 80 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 81 |
+
b, c, h, w = x.shape
|
| 82 |
+
k = self.kernel.expand(c, 1, 3, 3)
|
| 83 |
+
return F.conv2d(x, k, stride=2, padding=1, groups=c)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class DiscriminatorStage(nn.Module):
|
| 87 |
+
"""[ResidualBlock × N] + Downsample"""
|
| 88 |
+
|
| 89 |
+
def __init__(self, channels: int, num_blocks: int = 2, downsample: bool = True):
|
| 90 |
+
super().__init__()
|
| 91 |
+
self.blocks = nn.Sequential(*[ResidualBlock(channels) for _ in range(num_blocks)])
|
| 92 |
+
self.down = DownsampleLayer() if downsample else nn.Identity()
|
| 93 |
+
|
| 94 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 95 |
+
return self.down(self.blocks(x))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class R3GANDiscriminator(nn.Module):
|
| 99 |
+
"""
|
| 100 |
+
Anima latent-space text-conditional discriminator。
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
latent_channels: VAE latent channels (Anima: 16)
|
| 104 |
+
widths: stage 毎の channel 数 (粗→細の解像度遷移とは逆順)
|
| 105 |
+
blocks_per_stage: 各 stage の ResidualBlock 数
|
| 106 |
+
cond_dim: text embedding 次元(LLMAdapter 出力次元 = 1024)
|
| 107 |
+
cond_embedding_dim: projection D の中間次元
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def __init__(
|
| 111 |
+
self,
|
| 112 |
+
latent_channels: int = 16,
|
| 113 |
+
widths: tuple[int, ...] = (128, 256, 512, 768),
|
| 114 |
+
blocks_per_stage: int = 2,
|
| 115 |
+
cond_dim: int = 1024,
|
| 116 |
+
cond_embedding_dim: int = 512,
|
| 117 |
+
):
|
| 118 |
+
super().__init__()
|
| 119 |
+
# Extraction: latent -> initial channels
|
| 120 |
+
self.extract = nn.Conv2d(latent_channels, widths[0], kernel_size=1, bias=False)
|
| 121 |
+
msr_init(self.extract.weight, gain=1.0)
|
| 122 |
+
|
| 123 |
+
stages = []
|
| 124 |
+
for i, w in enumerate(widths):
|
| 125 |
+
next_w = widths[i + 1] if i + 1 < len(widths) else w
|
| 126 |
+
stages.append(DiscriminatorStage(w, num_blocks=blocks_per_stage,
|
| 127 |
+
downsample=(i < len(widths) - 1)))
|
| 128 |
+
if i < len(widths) - 1 and next_w != w:
|
| 129 |
+
# channel 変換 (1x1)
|
| 130 |
+
stages.append(nn.Conv2d(w, next_w, kernel_size=1, bias=False))
|
| 131 |
+
msr_init(stages[-1].weight, gain=1.0)
|
| 132 |
+
self.stages = nn.Sequential(*stages)
|
| 133 |
+
|
| 134 |
+
# Discriminative basis: 空間集約 -> per-sample feature vec
|
| 135 |
+
# 入力サイズによっては adaptive pool
|
| 136 |
+
self.spatial_pool = nn.AdaptiveAvgPool2d(4)
|
| 137 |
+
self.final_conv = nn.Conv2d(widths[-1], cond_embedding_dim, kernel_size=4, bias=False)
|
| 138 |
+
msr_init(self.final_conv.weight, gain=1.0)
|
| 139 |
+
|
| 140 |
+
# Projection conditioning (Miyato & Koyama 2018)
|
| 141 |
+
self.cond_proj = nn.Linear(cond_dim, cond_embedding_dim, bias=False)
|
| 142 |
+
msr_init(self.cond_proj.weight, gain=1.0 / math.sqrt(cond_embedding_dim))
|
| 143 |
+
|
| 144 |
+
def forward(self, latents: torch.Tensor, cond: torch.Tensor) -> torch.Tensor:
|
| 145 |
+
"""
|
| 146 |
+
latents: (B, 16, T, H_lat, W_lat) — T=1 想定なので squeeze
|
| 147 |
+
cond: (B, N_tokens, cond_dim) — 平均プールして使う
|
| 148 |
+
returns: (B,) critic value
|
| 149 |
+
"""
|
| 150 |
+
if latents.dim() == 5:
|
| 151 |
+
assert latents.shape[2] == 1, "static-image distillation: T must be 1"
|
| 152 |
+
latents = latents.squeeze(2) # (B, 16, H, W)
|
| 153 |
+
|
| 154 |
+
# weights dtype に揃える(generator 出力が float32 で来る場合あり)
|
| 155 |
+
w_dtype = next(self.parameters()).dtype
|
| 156 |
+
latents = latents.to(dtype=w_dtype)
|
| 157 |
+
cond = cond.to(dtype=w_dtype)
|
| 158 |
+
|
| 159 |
+
h = self.extract(latents)
|
| 160 |
+
h = self.stages(h)
|
| 161 |
+
h = self.spatial_pool(h)
|
| 162 |
+
feat = self.final_conv(h).flatten(1) # (B, cond_embedding_dim)
|
| 163 |
+
|
| 164 |
+
# text embedding 平均プール -> projection
|
| 165 |
+
cond_mean = cond.mean(dim=1) # (B, cond_dim)
|
| 166 |
+
cond_emb = self.cond_proj(cond_mean) # (B, cond_embedding_dim)
|
| 167 |
+
|
| 168 |
+
# projection-D logit = <feat, cond_emb>
|
| 169 |
+
logit = (feat * cond_emb).sum(dim=1) # (B,)
|
| 170 |
+
return logit
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# ---------- losses (RpGAN + R1 + R2) ----------------------------------------
|
| 174 |
+
|
| 175 |
+
def zero_centered_grad_penalty(samples: torch.Tensor, logits: torch.Tensor) -> torch.Tensor:
|
| 176 |
+
"""∑(∂D/∂x)^2 per sample."""
|
| 177 |
+
grads, = torch.autograd.grad(
|
| 178 |
+
outputs=logits.sum(), inputs=samples, create_graph=True,
|
| 179 |
+
)
|
| 180 |
+
return grads.flatten(1).square().sum(dim=1)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def d_loss_r3gan(
|
| 184 |
+
d_real_logits: torch.Tensor,
|
| 185 |
+
d_fake_logits: torch.Tensor,
|
| 186 |
+
real_samples: torch.Tensor,
|
| 187 |
+
fake_samples: torch.Tensor,
|
| 188 |
+
gamma: float = 50.0,
|
| 189 |
+
) -> tuple[torch.Tensor, dict]:
|
| 190 |
+
"""Discriminator loss = RpGAN softplus + (γ/2)(R1 + R2)."""
|
| 191 |
+
adv = F.softplus(-(d_real_logits - d_fake_logits)).mean()
|
| 192 |
+
r1 = zero_centered_grad_penalty(real_samples, d_real_logits).mean()
|
| 193 |
+
r2 = zero_centered_grad_penalty(fake_samples, d_fake_logits).mean()
|
| 194 |
+
loss = adv + 0.5 * gamma * (r1 + r2)
|
| 195 |
+
return loss, {"d_adv": adv.detach(), "d_r1": r1.detach(), "d_r2": r2.detach()}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def g_loss_r3gan(d_real_logits: torch.Tensor, d_fake_logits: torch.Tensor) -> torch.Tensor:
|
| 199 |
+
"""Generator loss (relativistic): softplus(-(D(fake) - D(real)))."""
|
| 200 |
+
return F.softplus(-(d_fake_logits - d_real_logits)).mean()
|
scripts/distill/shortcut_module.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shortcut Models 用の d-head injection。
|
| 3 |
+
|
| 4 |
+
設計:
|
| 5 |
+
- transformer.d_head: nn.Linear(1 → cond_dim) (zero-init、初期は影響ゼロ)
|
| 6 |
+
- transformer の t_embedder 出力に hook で d_head(d) を加算
|
| 7 |
+
- 推論/訓練時に transformer._current_d = d を set してから forward
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def find_t_embedder(transformer: nn.Module) -> tuple[str, nn.Module]:
|
| 15 |
+
"""transformer 内部の t_embedder を発見。短い path を優先。"""
|
| 16 |
+
candidates = []
|
| 17 |
+
for name, module in transformer.named_modules():
|
| 18 |
+
n = name.lower()
|
| 19 |
+
if ("t_embedder" in n or "time_embedder" in n or "timestep_embedder" in n) and \
|
| 20 |
+
("llm_adapter" not in name): # llm_adapter 内の embedder は除外
|
| 21 |
+
candidates.append((name, module))
|
| 22 |
+
if not candidates:
|
| 23 |
+
# fallback: find by "time" in name (more inclusive)
|
| 24 |
+
for name, module in transformer.named_modules():
|
| 25 |
+
if "time" in name.lower() and "llm_adapter" not in name and \
|
| 26 |
+
isinstance(module, (nn.Linear, nn.Sequential, nn.Module)) and \
|
| 27 |
+
len(list(module.parameters())) > 0:
|
| 28 |
+
candidates.append((name, module))
|
| 29 |
+
if not candidates:
|
| 30 |
+
raise RuntimeError("Could not find t_embedder module")
|
| 31 |
+
# shortest path
|
| 32 |
+
candidates.sort(key=lambda x: len(x[0].split(".")))
|
| 33 |
+
name, module = candidates[0]
|
| 34 |
+
print(f"[shortcut] found t_embedder at: {name}")
|
| 35 |
+
return name, module
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _detect_t_embed_dim(transformer: nn.Module, t_embedder: nn.Module) -> int:
|
| 39 |
+
"""t_embedder の output dim を dummy forward で推定。"""
|
| 40 |
+
device = next(t_embedder.parameters()).device
|
| 41 |
+
dtype = next(t_embedder.parameters()).dtype
|
| 42 |
+
with torch.no_grad():
|
| 43 |
+
# 多くの実装は (B,) or (B,1) の timestep を受ける
|
| 44 |
+
for shape in [(1,), (1, 1)]:
|
| 45 |
+
try:
|
| 46 |
+
t_in = torch.tensor([[0.5]] if len(shape) == 2 else [0.5],
|
| 47 |
+
device=device, dtype=dtype)
|
| 48 |
+
out = t_embedder(t_in)
|
| 49 |
+
if isinstance(out, tuple):
|
| 50 |
+
out = out[0]
|
| 51 |
+
return out.shape[-1]
|
| 52 |
+
except Exception:
|
| 53 |
+
continue
|
| 54 |
+
raise RuntimeError("Could not detect t_embedder output dim")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def attach_shortcut_d_head(transformer: nn.Module) -> nn.Module:
|
| 58 |
+
"""transformer に d_head を attach、t_embedder 出力に hook で加算する。
|
| 59 |
+
Returns: transformer (mutated)。`transformer._current_d` を set してから forward。"""
|
| 60 |
+
name, t_emb = find_t_embedder(transformer)
|
| 61 |
+
cond_dim = _detect_t_embed_dim(transformer, t_emb)
|
| 62 |
+
print(f"[shortcut] t_embedder output dim: {cond_dim}")
|
| 63 |
+
|
| 64 |
+
# d_head: 1 → cond_dim → cond_dim (small MLP, zero-init last layer)
|
| 65 |
+
device = next(t_emb.parameters()).device
|
| 66 |
+
dtype = next(t_emb.parameters()).dtype
|
| 67 |
+
d_head = nn.Sequential(
|
| 68 |
+
nn.Linear(1, cond_dim),
|
| 69 |
+
nn.SiLU(),
|
| 70 |
+
nn.Linear(cond_dim, cond_dim),
|
| 71 |
+
).to(device=device, dtype=dtype)
|
| 72 |
+
# zero-init last linear so initial output is 0
|
| 73 |
+
nn.init.zeros_(d_head[-1].weight)
|
| 74 |
+
nn.init.zeros_(d_head[-1].bias)
|
| 75 |
+
|
| 76 |
+
transformer.d_head = d_head
|
| 77 |
+
transformer._current_d = None # set per-forward
|
| 78 |
+
|
| 79 |
+
def _hook(module, input, output):
|
| 80 |
+
d = getattr(transformer, "_current_d", None)
|
| 81 |
+
if d is None:
|
| 82 |
+
return output
|
| 83 |
+
# output shape: (B, cond_dim) or (B, 1, cond_dim) etc
|
| 84 |
+
d_in = d.view(-1, 1).to(device=output.device if hasattr(output, 'device') else next(d_head.parameters()).device,
|
| 85 |
+
dtype=next(d_head.parameters()).dtype)
|
| 86 |
+
d_emb = d_head(d_in) # (B, cond_dim)
|
| 87 |
+
# broadcast to match output shape
|
| 88 |
+
if isinstance(output, tuple):
|
| 89 |
+
base = output[0]
|
| 90 |
+
d_emb_b = d_emb.view(base.size(0), *([1] * (base.dim() - 2)), base.size(-1)) if base.dim() > 2 else d_emb
|
| 91 |
+
return (base + d_emb_b.to(dtype=base.dtype),) + output[1:]
|
| 92 |
+
else:
|
| 93 |
+
d_emb_b = d_emb.view(output.size(0), *([1] * (output.dim() - 2)), output.size(-1)) if output.dim() > 2 else d_emb
|
| 94 |
+
return output + d_emb_b.to(dtype=output.dtype)
|
| 95 |
+
|
| 96 |
+
handle = t_emb.register_forward_hook(_hook)
|
| 97 |
+
transformer._d_head_hook_handle = handle
|
| 98 |
+
|
| 99 |
+
return transformer
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def set_shortcut_d(transformer: nn.Module, d: torch.Tensor | None):
|
| 103 |
+
"""forward 前に d を set。d=None で d-head 効果無効。"""
|
| 104 |
+
transformer._current_d = d
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def shortcut_d_head_params(transformer: nn.Module) -> list[nn.Parameter]:
|
| 108 |
+
"""d_head の trainable params (LoRA とは別の optimizer に渡す用)。"""
|
| 109 |
+
if hasattr(transformer, "d_head"):
|
| 110 |
+
return list(transformer.d_head.parameters())
|
| 111 |
+
return []
|
scripts/distill/sid_loss.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SiD2 / SiD-DiT 流派 (Score Identity Distillation) for Anima
|
| 3 |
+
|
| 4 |
+
Reference:
|
| 5 |
+
- arXiv 2404.04057 (original SiD)
|
| 6 |
+
- arXiv 2509.25127 (SiD-DiT、RF + TrigFlow native 適応)
|
| 7 |
+
- github.com/mingyuanzhou/SiD-LSG (SD 用 reference)
|
| 8 |
+
|
| 9 |
+
特徴:
|
| 10 |
+
- **data-free**: caption だけあれば画像不要
|
| 11 |
+
- critic 不要 (D 自体が存在しない)
|
| 12 |
+
- EMA 不要
|
| 13 |
+
- 2 LoRA pattern (generator + fake_score) を DMD2 と共有
|
| 14 |
+
- SiD identity formula で **mean collapse をスコア空間で防ぐ**
|
| 15 |
+
- RF (rectified flow) を直接サポート (teacher の再学習不要)
|
| 16 |
+
|
| 17 |
+
Generator loss (SiD-DiT, Eq. paper 2509.25127):
|
| 18 |
+
w(t) = (1 - t)
|
| 19 |
+
x_g = student rollout (n-step, last-step grad only)
|
| 20 |
+
x_t = (1-t) * x_g + t * eps
|
| 21 |
+
x0_φ = teacher(x_t, t, c) [+ CFG]
|
| 22 |
+
x0_ψ = fake_score(x_t, t, c)
|
| 23 |
+
|
| 24 |
+
diff_main = x0_φ - x0_ψ ← SiD core
|
| 25 |
+
diff_corr = x0_ψ - x_g ← identity correction
|
| 26 |
+
L_θ = (1-α) * w(t) * ||x_g - x0_φ||²
|
| 27 |
+
+ w(t) * <diff_main, diff_corr>
|
| 28 |
+
(per-sample mean over spatial dims)
|
| 29 |
+
|
| 30 |
+
Score helper loss (flow-matching MSE):
|
| 31 |
+
L_ψ = ||fake_score(x_t, t, c) - x_g.detach()||² ← simple distillation of x_g distribution
|
| 32 |
+
"""
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
from typing import Callable
|
| 35 |
+
|
| 36 |
+
import torch
|
| 37 |
+
import torch.nn.functional as F
|
| 38 |
+
|
| 39 |
+
from .anima_loader import AnimaBundle
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ----- RF helpers ----------------------------------------------------------
|
| 43 |
+
def renoise_rf(x0: torch.Tensor, t_rf: torch.Tensor, noise: torch.Tensor) -> torch.Tensor:
|
| 44 |
+
"""rectified flow forward: x_t = (1-t)*x0 + t*noise"""
|
| 45 |
+
t_ = t_rf.view(-1, *([1] * (x0.dim() - 1)))
|
| 46 |
+
return (1.0 - t_) * x0 + t_ * noise
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def x0_from_velocity_rf(x_t: torch.Tensor, v: torch.Tensor, t_rf: torch.Tensor) -> torch.Tensor:
|
| 50 |
+
"""x0 = x_t - t * v (v = noise - x0)"""
|
| 51 |
+
t_ = t_rf.view(-1, *([1] * (x_t.dim() - 1)))
|
| 52 |
+
return x_t - t_ * v
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# ----- time sampler --------------------------------------------------------
|
| 56 |
+
def sample_logit_normal_t(B: int, mu: float = 0.6931, sigma: float = 1.6,
|
| 57 |
+
device=None, dtype=torch.float32) -> torch.Tensor:
|
| 58 |
+
"""LogitNormal(mu=ln 2, sigma=1.6) on t ∈ (0, 1)。
|
| 59 |
+
SiD-DiT 流の time sampler、mid-noise (t≈0.66) に分布が集中。"""
|
| 60 |
+
z = torch.randn(B, device=device, dtype=dtype) * sigma + mu
|
| 61 |
+
t = torch.sigmoid(z) # (0, 1)
|
| 62 |
+
return t.clamp(1e-3, 1.0 - 1e-3)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# ----- few-step student rollout (DMD2 と同じパターン) ----------------------
|
| 66 |
+
def backward_simulation_grad_last(
|
| 67 |
+
student_v_fn: Callable[..., torch.Tensor],
|
| 68 |
+
noise: torch.Tensor,
|
| 69 |
+
n_steps: int,
|
| 70 |
+
cond_pos: torch.Tensor,
|
| 71 |
+
cond_neg: torch.Tensor | None,
|
| 72 |
+
cfg_scale: float = 1.0,
|
| 73 |
+
) -> torch.Tensor:
|
| 74 |
+
"""t=1 → t=0 へ Euler n_steps、grad は last step のみ。"""
|
| 75 |
+
from .traj_loss import cfg_guided, _broadcast_t
|
| 76 |
+
B = noise.size(0)
|
| 77 |
+
device = noise.device
|
| 78 |
+
dtype = noise.dtype
|
| 79 |
+
ts = torch.linspace(1.0, 0.0, n_steps + 1, device=device, dtype=torch.float32)
|
| 80 |
+
x = noise
|
| 81 |
+
for i in range(n_steps):
|
| 82 |
+
t_cur, t_next = ts[i], ts[i + 1]
|
| 83 |
+
is_last = (i == n_steps - 1)
|
| 84 |
+
ctx = torch.enable_grad() if is_last else torch.no_grad()
|
| 85 |
+
t_in = _broadcast_t(t_cur, B, device, dtype)
|
| 86 |
+
with ctx:
|
| 87 |
+
v = cfg_guided(student_v_fn, x, t_in, cond_pos, cond_neg, cfg_scale)
|
| 88 |
+
dt = (t_next - t_cur).to(device=device, dtype=dtype)
|
| 89 |
+
x = x + dt * v
|
| 90 |
+
return x
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ----- generator loss ------------------------------------------------------
|
| 94 |
+
def sid_generator_loss(
|
| 95 |
+
student_v_fn: Callable,
|
| 96 |
+
teacher_v_fn: Callable,
|
| 97 |
+
fake_score_v_fn: Callable,
|
| 98 |
+
init_noise: torch.Tensor,
|
| 99 |
+
cond_pos: torch.Tensor,
|
| 100 |
+
cond_neg: torch.Tensor | None,
|
| 101 |
+
teacher_cfg: float = 4.5,
|
| 102 |
+
student_cfg: float = 1.0,
|
| 103 |
+
n_steps: int = 4,
|
| 104 |
+
alpha: float = 1.2,
|
| 105 |
+
mu_t: float = 0.6931,
|
| 106 |
+
sigma_t: float = 1.6,
|
| 107 |
+
) -> tuple[torch.Tensor, dict]:
|
| 108 |
+
"""SiD-DiT generator loss。data-free、Anima RF native。"""
|
| 109 |
+
B = init_noise.size(0)
|
| 110 |
+
device = init_noise.device
|
| 111 |
+
dtype = init_noise.dtype
|
| 112 |
+
|
| 113 |
+
# 1) student rollout
|
| 114 |
+
x_g = backward_simulation_grad_last(
|
| 115 |
+
student_v_fn, init_noise, n_steps, cond_pos, cond_neg,
|
| 116 |
+
cfg_scale=student_cfg,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# 2) re-noise at sampled t
|
| 120 |
+
t = sample_logit_normal_t(B, mu_t, sigma_t, device, dtype=torch.float32)
|
| 121 |
+
D_eps = torch.randn_like(x_g)
|
| 122 |
+
x_t = renoise_rf(x_g, t, D_eps)
|
| 123 |
+
|
| 124 |
+
# 3) teacher x0 (CFG) and fake_score x0 (no_grad both)
|
| 125 |
+
with torch.no_grad():
|
| 126 |
+
t_in = t.to(dtype=dtype)
|
| 127 |
+
v_t_cond = teacher_v_fn(x_t, t_in, cond_pos)
|
| 128 |
+
if teacher_cfg > 1.0 and cond_neg is not None:
|
| 129 |
+
v_t_uncond = teacher_v_fn(x_t, t_in, cond_neg)
|
| 130 |
+
v_teacher = v_t_uncond + teacher_cfg * (v_t_cond - v_t_uncond)
|
| 131 |
+
else:
|
| 132 |
+
v_teacher = v_t_cond
|
| 133 |
+
x0_phi = x0_from_velocity_rf(x_t, v_teacher, t)
|
| 134 |
+
|
| 135 |
+
v_fake = fake_score_v_fn(x_t, t_in, cond_pos)
|
| 136 |
+
x0_psi = x0_from_velocity_rf(x_t, v_fake, t)
|
| 137 |
+
|
| 138 |
+
# 4) SiD-DiT fused loss
|
| 139 |
+
# w(t) = (1 - t)、per-sample weight
|
| 140 |
+
w = (1.0 - t).clamp(min=0.05).view(-1, *([1] * (x_g.dim() - 1)))
|
| 141 |
+
diff_main = (x0_phi - x0_psi).detach()
|
| 142 |
+
diff_corr = (x0_psi - x_g).detach()
|
| 143 |
+
# term 1: baseline match (mean reverse-KL-like)
|
| 144 |
+
# ||x_g - x0_phi||² の grad は x_g を x0_phi に引き寄せる
|
| 145 |
+
term_baseline = ((x_g - x0_phi.detach()).float() ** 2)
|
| 146 |
+
# term 2: identity correction (negative for "push away from fake_score")
|
| 147 |
+
# <x_g - (x_g - identity_grad)> として書き直すと DMD2 trick と同じ形
|
| 148 |
+
# ここでは直接 SiD-DiT 表式: w * <diff_main, diff_corr> (定数項なので grad は 0)
|
| 149 |
+
# 実装上は generator が ψ - x_g 方向に動くよう、x_g に grad を流す:
|
| 150 |
+
grad_signal = (diff_main * diff_corr).detach() # 形状 (B, ...) 、generator に伝わる勾配は diff_main 方向
|
| 151 |
+
term_correction = - (x_g * (diff_main.detach())).float() # 内積の grad part (∂/∂x_g)
|
| 152 |
+
|
| 153 |
+
L_theta = (
|
| 154 |
+
(1.0 - alpha) * (w * term_baseline).mean()
|
| 155 |
+
+ (w * term_correction).mean()
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
metrics = {
|
| 159 |
+
"l_sid_gen": L_theta.detach(),
|
| 160 |
+
"t_mean": t.mean(),
|
| 161 |
+
"x_g_abs": x_g.detach().abs().mean(),
|
| 162 |
+
"x0_phi_abs": x0_phi.abs().mean(),
|
| 163 |
+
"x0_psi_abs": x0_psi.abs().mean(),
|
| 164 |
+
"diff_main_abs": diff_main.abs().mean(),
|
| 165 |
+
"w_mean": w.mean(),
|
| 166 |
+
}
|
| 167 |
+
return L_theta, metrics
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ----- score helper loss (fake_score の更新) -------------------------------
|
| 171 |
+
def sid_score_helper_loss(
|
| 172 |
+
student_v_fn: Callable,
|
| 173 |
+
fake_score_v_fn: Callable,
|
| 174 |
+
init_noise: torch.Tensor,
|
| 175 |
+
cond_pos: torch.Tensor,
|
| 176 |
+
cond_neg: torch.Tensor | None,
|
| 177 |
+
student_cfg: float = 1.0,
|
| 178 |
+
n_steps: int = 4,
|
| 179 |
+
mu_t: float = 0.6931,
|
| 180 |
+
sigma_t: float = 1.6,
|
| 181 |
+
) -> tuple[torch.Tensor, dict]:
|
| 182 |
+
"""fake_score を student 分布の denoiser として学習。
|
| 183 |
+
student 側は no_grad、fake_score 側に grad。"""
|
| 184 |
+
B = init_noise.size(0)
|
| 185 |
+
device = init_noise.device
|
| 186 |
+
dtype = init_noise.dtype
|
| 187 |
+
|
| 188 |
+
# 1) student rollout (no_grad)
|
| 189 |
+
with torch.no_grad():
|
| 190 |
+
x_g = backward_simulation_grad_last(
|
| 191 |
+
# all no_grad: 内部の last-step grad はもとから no_grad コンテキストでマスク
|
| 192 |
+
student_v_fn, init_noise, n_steps, cond_pos, cond_neg,
|
| 193 |
+
cfg_scale=student_cfg,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# 2) re-noise
|
| 197 |
+
t = sample_logit_normal_t(B, mu_t, sigma_t, device, dtype=torch.float32)
|
| 198 |
+
D_eps = torch.randn_like(x_g)
|
| 199 |
+
x_t = renoise_rf(x_g, t, D_eps)
|
| 200 |
+
|
| 201 |
+
# 3) fake_score x0 prediction (grad on)
|
| 202 |
+
t_in = t.to(dtype=dtype)
|
| 203 |
+
v_psi = fake_score_v_fn(x_t, t_in, cond_pos)
|
| 204 |
+
x0_psi = x0_from_velocity_rf(x_t, v_psi, t)
|
| 205 |
+
|
| 206 |
+
# 4) plain MSE to x_g (denoising target)
|
| 207 |
+
L_psi = F.mse_loss(x0_psi.float(), x_g.detach().float())
|
| 208 |
+
|
| 209 |
+
return L_psi, {"l_sid_psi": L_psi.detach()}
|
scripts/distill/train_dmd2_official.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima DMD2 distillation (NVIDIA cosmos-predict2.5 official 流派)
|
| 4 |
+
==============================================================
|
| 5 |
+
|
| 6 |
+
特徴:
|
| 7 |
+
- 同じ base 2B DiT に **2 つの独立 PEFT adapter** を attach (student / fake_score)
|
| 8 |
+
- peft.PeftModel.add_adapter() + set_adapter() で runtime 切替
|
| 9 |
+
- Alternating: critic × N_critic (=5) → generator × 1
|
| 10 |
+
- few-step student rollout (1-4 step)、grad は最終 step だけ通す (memory efficient)
|
| 11 |
+
- teacher は frozen base (no LoRA、deepcopy で別 instance)
|
| 12 |
+
|
| 13 |
+
要点:
|
| 14 |
+
- student / fake_score の grad は **独立した optimizer** で更新
|
| 15 |
+
- 各 step 開始時に明示的に set_adapter() で切替
|
| 16 |
+
- teacher は別 module、active adapter 概念とは無関係
|
| 17 |
+
|
| 18 |
+
使い方 (Modal):
|
| 19 |
+
# Smoke (1 outer step、cycle=5 critic+1 gen)
|
| 20 |
+
modal run modal_app.py::train_dmd2_official_distill \\
|
| 21 |
+
--total-outer-steps 1 --resolution 768 --n-student-steps 4
|
| 22 |
+
|
| 23 |
+
# 本番 (5000 outer step、~$21)
|
| 24 |
+
modal run --detach modal_app.py::train_dmd2_official_distill \\
|
| 25 |
+
--total-outer-steps 5000 --resolution 768 --warm-lora /models/loras/anima_turbo.safetensors
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
import argparse
|
| 29 |
+
import copy
|
| 30 |
+
import json
|
| 31 |
+
import os
|
| 32 |
+
import sys
|
| 33 |
+
import time
|
| 34 |
+
from pathlib import Path
|
| 35 |
+
|
| 36 |
+
import torch
|
| 37 |
+
import peft
|
| 38 |
+
from torch.utils.data import DataLoader
|
| 39 |
+
from safetensors.torch import save_file, load_file
|
| 40 |
+
|
| 41 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 42 |
+
from distill.anima_loader import AnimaPaths, build_anima, AnimaBundle
|
| 43 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 44 |
+
from distill.dmd2_official_loss import (
|
| 45 |
+
dmd2_generator_loss, dmd2_critic_loss,
|
| 46 |
+
)
|
| 47 |
+
from distill.train_traj import (
|
| 48 |
+
TextOnlyDataset, text_collate,
|
| 49 |
+
convert_comfy_to_peft_lora, save_lora_state,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
# 2 adapter 管理ヘルパー
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
def attach_dual_lora(
|
| 57 |
+
transformer: torch.nn.Module,
|
| 58 |
+
rank: int = 32,
|
| 59 |
+
adapter_names: tuple[str, str] = ("student", "fake_score"),
|
| 60 |
+
):
|
| 61 |
+
"""同じ base DiT に 2 つの独立 LoRA adapter を attach する。
|
| 62 |
+
PEFT は LoraConfig + adapter_name で複数 adapter を保持できる。
|
| 63 |
+
set_adapter(name) で active adapter を切替。
|
| 64 |
+
"""
|
| 65 |
+
# まず 1 つ目の adapter を attach (peft.get_peft_model がベース wrap)
|
| 66 |
+
target_modules = []
|
| 67 |
+
for name, module in transformer.named_modules():
|
| 68 |
+
if not isinstance(module, torch.nn.Linear):
|
| 69 |
+
continue
|
| 70 |
+
if "llm_adapter" in name:
|
| 71 |
+
continue
|
| 72 |
+
target_modules.append(name)
|
| 73 |
+
print(f"[lora] wide target: {len(target_modules)} linear modules")
|
| 74 |
+
|
| 75 |
+
cfg = peft.LoraConfig(
|
| 76 |
+
r=rank, lora_alpha=rank, lora_dropout=0.0, bias="none",
|
| 77 |
+
target_modules=target_modules,
|
| 78 |
+
)
|
| 79 |
+
peft_model = peft.get_peft_model(transformer, cfg, adapter_name=adapter_names[0])
|
| 80 |
+
# 2 つ目を追加
|
| 81 |
+
peft_model.add_adapter(adapter_names[1], cfg)
|
| 82 |
+
print(f"[lora] attached adapters: {adapter_names}")
|
| 83 |
+
return peft_model
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def make_velocity_fn(peft_model, adapter_name: str):
|
| 87 |
+
"""指定 adapter を active にして dit_forward する callable を返す。"""
|
| 88 |
+
def _v(x, t, cond):
|
| 89 |
+
# 毎 forward で adapter を確実に切替 (PEFT は内部で全モジュールに反映)
|
| 90 |
+
peft_model.set_adapter(adapter_name)
|
| 91 |
+
return AnimaBundle.dit_forward(peft_model, x, t, cond)
|
| 92 |
+
return _v
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def make_teacher_velocity_fn(teacher_transformer):
|
| 96 |
+
"""frozen teacher 用 (LoRA なし、別 module instance)。"""
|
| 97 |
+
def _v(x, t, cond):
|
| 98 |
+
return AnimaBundle.dit_forward(teacher_transformer, x, t, cond)
|
| 99 |
+
return _v
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
# main
|
| 104 |
+
# ---------------------------------------------------------------------------
|
| 105 |
+
def main():
|
| 106 |
+
ap = argparse.ArgumentParser()
|
| 107 |
+
ap.add_argument("--dataset", required=True, type=str)
|
| 108 |
+
ap.add_argument("--out", required=True, type=str)
|
| 109 |
+
ap.add_argument("--warm-lora", default="", type=str,
|
| 110 |
+
help="Civitai Anima Turbo LoRA (ComfyUI 形式) を student adapter 初期値に")
|
| 111 |
+
ap.add_argument("--total-outer-steps", type=int, default=5000,
|
| 112 |
+
help="1 outer step = N_critic + 1 generator")
|
| 113 |
+
ap.add_argument("--n-critic-per-gen", type=int, default=5)
|
| 114 |
+
ap.add_argument("--n-student-steps", type=int, default=4,
|
| 115 |
+
help="few-step rollout の step 数 (1-4)、1=single-step distill")
|
| 116 |
+
ap.add_argument("--batch-size", type=int, default=1)
|
| 117 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 118 |
+
ap.add_argument("--teacher-cfg", type=float, default=3.0)
|
| 119 |
+
ap.add_argument("--student-cfg", type=float, default=1.0)
|
| 120 |
+
ap.add_argument("--shift", type=float, default=5.0,
|
| 121 |
+
help="shifted uniform sampling shift (high-noise 側に偏らせる)")
|
| 122 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 123 |
+
ap.add_argument("--lr-gen", type=float, default=5e-6)
|
| 124 |
+
ap.add_argument("--lr-critic", type=float, default=1e-5)
|
| 125 |
+
ap.add_argument("--weight-decay", type=float, default=0.01)
|
| 126 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 127 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 128 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 129 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 130 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 131 |
+
ap.add_argument("--neg-prompt", default="")
|
| 132 |
+
args = ap.parse_args()
|
| 133 |
+
|
| 134 |
+
torch.manual_seed(args.seed)
|
| 135 |
+
device = torch.device("cuda")
|
| 136 |
+
dtype = torch.bfloat16
|
| 137 |
+
|
| 138 |
+
out_dir = Path(args.out)
|
| 139 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 140 |
+
|
| 141 |
+
# ----- load Anima base -----
|
| 142 |
+
print("[load] Anima bundle")
|
| 143 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 144 |
+
|
| 145 |
+
# ----- teacher = deepcopy (frozen, no LoRA) -----
|
| 146 |
+
print("[setup] deepcopy DiT for teacher (frozen, no LoRA)")
|
| 147 |
+
teacher_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype).eval()
|
| 148 |
+
for p in teacher_transformer.parameters():
|
| 149 |
+
p.requires_grad = False
|
| 150 |
+
|
| 151 |
+
# ----- 元 DiT (= bundle.transformer) に 2 adapter を attach -----
|
| 152 |
+
print("[setup] dual-adapter on base DiT (student + fake_score)")
|
| 153 |
+
peft_model = attach_dual_lora(bundle.transformer, rank=args.lora_rank,
|
| 154 |
+
adapter_names=("student", "fake_score"))
|
| 155 |
+
peft_model.to(device=device, dtype=dtype)
|
| 156 |
+
# base 凍結、LoRA だけ trainable
|
| 157 |
+
for n, p in peft_model.named_parameters():
|
| 158 |
+
p.requires_grad = ("lora_" in n)
|
| 159 |
+
bundle.transformer = peft_model
|
| 160 |
+
|
| 161 |
+
# 各 adapter の trainable params を log
|
| 162 |
+
student_params = [p for n, p in peft_model.named_parameters() if p.requires_grad and ".student." in n]
|
| 163 |
+
fake_params = [p for n, p in peft_model.named_parameters() if p.requires_grad and ".fake_score." in n]
|
| 164 |
+
print(f"[setup] student trainable: {sum(p.numel() for p in student_params)/1e6:.1f}M")
|
| 165 |
+
print(f"[setup] fake_score trainable: {sum(p.numel() for p in fake_params)/1e6:.1f}M")
|
| 166 |
+
|
| 167 |
+
# warm-start: Civitai Anima Turbo を student adapter に注入
|
| 168 |
+
if args.warm_lora:
|
| 169 |
+
print(f"[warm] loading {args.warm_lora} → student adapter")
|
| 170 |
+
# PEFT の 2-adapter 形式: key = base_model.model.<...>.lora_A.<adapter_name>.weight
|
| 171 |
+
sd_comfy = load_file(args.warm_lora)
|
| 172 |
+
sd_peft_default = convert_comfy_to_peft_lora(sd_comfy) # adapter_name="default" 想定
|
| 173 |
+
# "default" → "student" にリネーム
|
| 174 |
+
sd_student = {}
|
| 175 |
+
for k, v in sd_peft_default.items():
|
| 176 |
+
nk = k.replace(".lora_A.default.weight", ".lora_A.student.weight")
|
| 177 |
+
nk = nk.replace(".lora_B.default.weight", ".lora_B.student.weight")
|
| 178 |
+
sd_student[nk] = v
|
| 179 |
+
# filter shape mismatch
|
| 180 |
+
model_sd = peft_model.state_dict()
|
| 181 |
+
to_load = {k: v.to(dtype=model_sd[k].dtype) for k, v in sd_student.items()
|
| 182 |
+
if k in model_sd and model_sd[k].shape == v.shape}
|
| 183 |
+
print(f"[warm] matched {len(to_load)}/{len(sd_student)} keys")
|
| 184 |
+
peft_model.load_state_dict(to_load, strict=False)
|
| 185 |
+
|
| 186 |
+
# ----- optimizers (student / critic 独立) -----
|
| 187 |
+
opt_gen = torch.optim.AdamW(student_params, lr=args.lr_gen, betas=(0.9, 0.999),
|
| 188 |
+
weight_decay=args.weight_decay, eps=1e-8)
|
| 189 |
+
opt_critic = torch.optim.AdamW(fake_params, lr=args.lr_critic, betas=(0.9, 0.999),
|
| 190 |
+
weight_decay=args.weight_decay, eps=1e-8)
|
| 191 |
+
|
| 192 |
+
# ----- velocity functions -----
|
| 193 |
+
student_v = make_velocity_fn(peft_model, "student")
|
| 194 |
+
fake_score_v = make_velocity_fn(peft_model, "fake_score")
|
| 195 |
+
teacher_v = make_teacher_velocity_fn(teacher_transformer)
|
| 196 |
+
|
| 197 |
+
# ----- dataset -----
|
| 198 |
+
print(f"[data] loading {args.dataset}")
|
| 199 |
+
dataset = TextOnlyDataset(args.dataset)
|
| 200 |
+
print(f" {len(dataset)} captions")
|
| 201 |
+
loader = DataLoader(
|
| 202 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 203 |
+
num_workers=args.num_workers, collate_fn=text_collate, drop_last=True,
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
# ----- conditioning preencoded once for negative -----
|
| 207 |
+
with torch.no_grad():
|
| 208 |
+
cond_neg = bundle.text_encode([args.neg_prompt or ""])
|
| 209 |
+
|
| 210 |
+
H_lat = args.resolution // 8
|
| 211 |
+
W_lat = args.resolution // 8
|
| 212 |
+
|
| 213 |
+
# ----- training loop -----
|
| 214 |
+
print(f"[train] outer={args.total_outer_steps} cycle=({args.n_critic_per_gen} critic + 1 gen) "
|
| 215 |
+
f"n_student_steps={args.n_student_steps} res={args.resolution}")
|
| 216 |
+
log_path = out_dir / "dmd2_log.jsonl"
|
| 217 |
+
log_f = open(log_path, "a", buffering=1)
|
| 218 |
+
t0 = time.time()
|
| 219 |
+
data_iter = iter(loader)
|
| 220 |
+
|
| 221 |
+
def _next_batch():
|
| 222 |
+
nonlocal data_iter
|
| 223 |
+
try:
|
| 224 |
+
return next(data_iter)
|
| 225 |
+
except StopIteration:
|
| 226 |
+
data_iter = iter(loader)
|
| 227 |
+
return next(data_iter)
|
| 228 |
+
|
| 229 |
+
def _sample_inputs():
|
| 230 |
+
captions = _next_batch()
|
| 231 |
+
B = len(captions)
|
| 232 |
+
with torch.no_grad():
|
| 233 |
+
cond_pos = bundle.text_encode(captions)
|
| 234 |
+
cond_neg_b = cond_neg.expand(B, -1, -1).contiguous() if B > 1 else cond_neg
|
| 235 |
+
noise = torch.randn(B, 16, 1, H_lat, W_lat, device=device, dtype=dtype)
|
| 236 |
+
return noise, cond_pos, cond_neg_b
|
| 237 |
+
|
| 238 |
+
for outer in range(args.total_outer_steps):
|
| 239 |
+
# ---- N critic updates ----
|
| 240 |
+
critic_metrics = {}
|
| 241 |
+
for _ in range(args.n_critic_per_gen):
|
| 242 |
+
noise, cond_pos, cond_neg_b = _sample_inputs()
|
| 243 |
+
peft_model.train()
|
| 244 |
+
loss_c, m_c = dmd2_critic_loss(
|
| 245 |
+
student_v, fake_score_v, noise, cond_pos, cond_neg_b,
|
| 246 |
+
student_cfg=args.student_cfg, n_steps=args.n_student_steps,
|
| 247 |
+
shift=args.shift,
|
| 248 |
+
)
|
| 249 |
+
opt_critic.zero_grad()
|
| 250 |
+
loss_c.backward()
|
| 251 |
+
torch.nn.utils.clip_grad_norm_(fake_params, args.grad_clip)
|
| 252 |
+
opt_critic.step()
|
| 253 |
+
critic_metrics = {k: float(v) for k, v in m_c.items()}
|
| 254 |
+
|
| 255 |
+
# ---- 1 generator update ----
|
| 256 |
+
noise, cond_pos, cond_neg_b = _sample_inputs()
|
| 257 |
+
peft_model.train()
|
| 258 |
+
loss_g, m_g = dmd2_generator_loss(
|
| 259 |
+
student_v, teacher_v, fake_score_v, noise, cond_pos, cond_neg_b,
|
| 260 |
+
teacher_cfg=args.teacher_cfg, student_cfg=args.student_cfg,
|
| 261 |
+
n_steps=args.n_student_steps, shift=args.shift,
|
| 262 |
+
)
|
| 263 |
+
opt_gen.zero_grad()
|
| 264 |
+
loss_g.backward()
|
| 265 |
+
torch.nn.utils.clip_grad_norm_(student_params, args.grad_clip)
|
| 266 |
+
opt_gen.step()
|
| 267 |
+
gen_metrics = {k: float(v) for k, v in m_g.items()}
|
| 268 |
+
|
| 269 |
+
# ---- log ----
|
| 270 |
+
if outer % args.log_every == 0:
|
| 271 |
+
metrics = {"outer": outer, "elapsed": time.time() - t0,
|
| 272 |
+
**critic_metrics, **gen_metrics}
|
| 273 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 274 |
+
msg = " ".join(f"{k}={v:.4f}" for k, v in metrics.items() if k not in ("outer",))
|
| 275 |
+
print(f"[outer {outer}/{args.total_outer_steps}] {msg}", flush=True)
|
| 276 |
+
|
| 277 |
+
# ---- checkpoint (student adapter のみ保存) ----
|
| 278 |
+
if outer > 0 and outer % args.sample_every == 0:
|
| 279 |
+
# student adapter のみを抽出
|
| 280 |
+
sd = {k: v.detach().cpu() for k, v in peft_model.state_dict().items()
|
| 281 |
+
if "lora_" in k and ".student." in k}
|
| 282 |
+
from safetensors.torch import save_file as _sf
|
| 283 |
+
(out_dir).mkdir(parents=True, exist_ok=True)
|
| 284 |
+
_sf(sd, str(out_dir / f"dmd2_student_step{outer:05d}.safetensors"))
|
| 285 |
+
print(f"[save] dmd2_student_step{outer:05d}.safetensors", flush=True)
|
| 286 |
+
try:
|
| 287 |
+
import modal
|
| 288 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 289 |
+
except Exception as e:
|
| 290 |
+
print(f"[save] volume commit failed: {e}", flush=True)
|
| 291 |
+
|
| 292 |
+
# final
|
| 293 |
+
print("[done] saving final adapters")
|
| 294 |
+
sd_student_final = {k: v.detach().cpu() for k, v in peft_model.state_dict().items()
|
| 295 |
+
if "lora_" in k and ".student." in k}
|
| 296 |
+
from safetensors.torch import save_file as _sf
|
| 297 |
+
_sf(sd_student_final, str(out_dir / "dmd2_student_final.safetensors"))
|
| 298 |
+
sd_fake_final = {k: v.detach().cpu() for k, v in peft_model.state_dict().items()
|
| 299 |
+
if "lora_" in k and ".fake_score." in k}
|
| 300 |
+
_sf(sd_fake_final, str(out_dir / "dmd2_fake_score_final.safetensors"))
|
| 301 |
+
log_f.close()
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
if __name__ == "__main__":
|
| 305 |
+
main()
|
scripts/distill/train_dmdx.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima DMDX (ADM-only) 蒸留 — arxiv 2507.18569v1 移植
|
| 4 |
+
=====================================================
|
| 5 |
+
|
| 6 |
+
Pipeline:
|
| 7 |
+
- student: wide LoRA on Anima base (DMD2 と同じ wide target、~72M trainable)
|
| 8 |
+
- discriminator: LADD-style (teacher MiniTrainDIT frozen backbone + spectral norm heads)
|
| 9 |
+
- real: teacher_x0_cache の x0 を流用
|
| 10 |
+
- ADM loss: hinge GAN at t-Δt after teacher 1-step evolution
|
| 11 |
+
- time schedule: cubic high-noise bias (paper default)
|
| 12 |
+
- alternation: N_critic × disc → 1 × generator (DMD2 風)
|
| 13 |
+
|
| 14 |
+
DMD2 との違い:
|
| 15 |
+
- DMD2: dual adapter (student + fake_score)、reverse-KL grad trick
|
| 16 |
+
- DMDX: single student LoRA + 別 D heads、hinge GAN、TVD 最小化
|
| 17 |
+
|
| 18 |
+
オプション:
|
| 19 |
+
- --recon-weight > 0 で LADD 流 Smooth-L1 anchor 追加 (mean collapse 防止)
|
| 20 |
+
- --misaligned-pairs-d : LADD 流の text-alignment trick
|
| 21 |
+
|
| 22 |
+
Modal CLI 例:
|
| 23 |
+
modal run modal_app.py::train_dmdx_distill \\
|
| 24 |
+
--warm-lora /models/loras/anima_turbo.safetensors \\
|
| 25 |
+
--total-outer-steps 5000
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
import argparse
|
| 29 |
+
import copy
|
| 30 |
+
import json
|
| 31 |
+
import os
|
| 32 |
+
import sys
|
| 33 |
+
import time
|
| 34 |
+
from pathlib import Path
|
| 35 |
+
|
| 36 |
+
import torch
|
| 37 |
+
from torch.utils.data import DataLoader
|
| 38 |
+
|
| 39 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 40 |
+
from distill.anima_loader import AnimaPaths, build_anima, AnimaBundle
|
| 41 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 42 |
+
from distill.train_traj import save_lora_state
|
| 43 |
+
from distill.train_ladd import PrecomputedCacheDataset, ladd_collate
|
| 44 |
+
from distill.anima_ladd_disc import AnimaLADDDiscriminator
|
| 45 |
+
from distill.dmdx_loss import adm_generator_loss, adm_discriminator_loss
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def main():
|
| 49 |
+
ap = argparse.ArgumentParser()
|
| 50 |
+
ap.add_argument("--cache-dir", required=True, type=str,
|
| 51 |
+
help="teacher_x0_cache (real x0 source)")
|
| 52 |
+
ap.add_argument("--out", required=True, type=str)
|
| 53 |
+
ap.add_argument("--warm-lora", default="", type=str)
|
| 54 |
+
ap.add_argument("--total-outer-steps", type=int, default=5000,
|
| 55 |
+
help="1 outer = N_critic + 1 generator")
|
| 56 |
+
ap.add_argument("--n-critic-per-gen", type=int, default=2,
|
| 57 |
+
help="DMDX paper: hinge GAN は 1:1〜2:1 が一般的 (DMD2 の 5:1 より少ない)")
|
| 58 |
+
ap.add_argument("--n-student-steps", type=int, default=4,
|
| 59 |
+
help="few-step rollout step 数")
|
| 60 |
+
ap.add_argument("--batch-size", type=int, default=1)
|
| 61 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 62 |
+
ap.add_argument("--teacher-cfg", type=float, default=4.5,
|
| 63 |
+
help="Anima 公式推奨 CFG")
|
| 64 |
+
ap.add_argument("--student-cfg", type=float, default=1.0)
|
| 65 |
+
ap.add_argument("--dt-ratio", type=float, default=1.0 / 64,
|
| 66 |
+
help="paper default Δt = T/64")
|
| 67 |
+
ap.add_argument("--recon-weight", type=float, default=0.0,
|
| 68 |
+
help="LADD 流 Smooth-L1 anchor (>0 で有効化、stability boost)")
|
| 69 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 70 |
+
ap.add_argument("--lr-gen", type=float, default=5e-6)
|
| 71 |
+
ap.add_argument("--lr-disc", type=float, default=1e-5)
|
| 72 |
+
ap.add_argument("--weight-decay", type=float, default=0.01)
|
| 73 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 74 |
+
ap.add_argument("--block-ids", type=str, default="2,8,14,20,26",
|
| 75 |
+
help="teacher block indices for D hooks")
|
| 76 |
+
ap.add_argument("--head-hidden", type=int, default=512)
|
| 77 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 78 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 79 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 80 |
+
ap.add_argument("--neg-prompt", default="")
|
| 81 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 82 |
+
args = ap.parse_args()
|
| 83 |
+
|
| 84 |
+
torch.manual_seed(args.seed)
|
| 85 |
+
device = torch.device("cuda")
|
| 86 |
+
dtype = torch.bfloat16
|
| 87 |
+
out_dir = Path(args.out)
|
| 88 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 89 |
+
|
| 90 |
+
# ----- load Anima base -----
|
| 91 |
+
print("[load] Anima bundle")
|
| 92 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 93 |
+
|
| 94 |
+
# ----- teacher = deepcopy frozen -----
|
| 95 |
+
print("[setup] teacher = frozen deepcopy")
|
| 96 |
+
teacher_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype).eval()
|
| 97 |
+
for p in teacher_transformer.parameters():
|
| 98 |
+
p.requires_grad = False
|
| 99 |
+
|
| 100 |
+
# ----- student = wide LoRA on bundle.transformer -----
|
| 101 |
+
print("[setup] student = wide LoRA")
|
| 102 |
+
student_transformer = attach_wide_lora(bundle.transformer, rank=args.lora_rank)
|
| 103 |
+
student_transformer.to(device=device, dtype=dtype)
|
| 104 |
+
for n, p in student_transformer.named_parameters():
|
| 105 |
+
p.requires_grad = ("lora_" in n)
|
| 106 |
+
student_params = [p for p in student_transformer.parameters() if p.requires_grad]
|
| 107 |
+
print(f"[setup] student trainable: {sum(p.numel() for p in student_params)/1e6:.1f}M")
|
| 108 |
+
bundle.transformer = student_transformer
|
| 109 |
+
|
| 110 |
+
# warm-start
|
| 111 |
+
if args.warm_lora:
|
| 112 |
+
from distill.train_traj import load_warm_lora
|
| 113 |
+
load_warm_lora(student_transformer, args.warm_lora)
|
| 114 |
+
|
| 115 |
+
# ----- discriminator (LADD-style) -----
|
| 116 |
+
print("[setup] DMDX discriminator (teacher backbone + spectral heads)")
|
| 117 |
+
block_ids = [int(x) for x in args.block_ids.split(",")]
|
| 118 |
+
disc = AnimaLADDDiscriminator(
|
| 119 |
+
teacher_transformer=teacher_transformer,
|
| 120 |
+
block_ids=block_ids, head_hidden=args.head_hidden,
|
| 121 |
+
)
|
| 122 |
+
H_lat = args.resolution // 8
|
| 123 |
+
W_lat = args.resolution // 8
|
| 124 |
+
dummy_x = torch.randn(1, 16, 1, H_lat, W_lat, device=device, dtype=dtype)
|
| 125 |
+
dummy_t = torch.tensor([0.5], device=device, dtype=dtype)
|
| 126 |
+
with torch.no_grad():
|
| 127 |
+
dummy_cond = bundle.text_encode([""])
|
| 128 |
+
disc.lazy_init_heads(dummy_x, dummy_t, dummy_cond)
|
| 129 |
+
disc.to(device=device, dtype=torch.float32)
|
| 130 |
+
disc_params = disc.trainable_parameters()
|
| 131 |
+
print(f"[setup] D heads trainable: {sum(p.numel() for p in disc_params)/1e6:.1f}M")
|
| 132 |
+
|
| 133 |
+
# ----- optimizers -----
|
| 134 |
+
opt_gen = torch.optim.AdamW(student_params, lr=args.lr_gen, betas=(0.0, 0.999),
|
| 135 |
+
weight_decay=args.weight_decay, eps=1e-8)
|
| 136 |
+
opt_disc = torch.optim.AdamW(disc_params, lr=args.lr_disc, betas=(0.0, 0.999),
|
| 137 |
+
weight_decay=args.weight_decay, eps=1e-8)
|
| 138 |
+
|
| 139 |
+
# ----- velocity functions -----
|
| 140 |
+
def student_v(x, t, cond):
|
| 141 |
+
return AnimaBundle.dit_forward(student_transformer, x, t, cond)
|
| 142 |
+
|
| 143 |
+
def teacher_v(x, t, cond):
|
| 144 |
+
return AnimaBundle.dit_forward(teacher_transformer, x, t, cond)
|
| 145 |
+
|
| 146 |
+
# ----- conditioning (neg prompt cached once) -----
|
| 147 |
+
with torch.no_grad():
|
| 148 |
+
cond_neg = bundle.text_encode([args.neg_prompt or ""])
|
| 149 |
+
|
| 150 |
+
# ----- dataset (precompute cache provides real x0 + caption emb) -----
|
| 151 |
+
print(f"[data] loading cache {args.cache_dir}")
|
| 152 |
+
dataset = PrecomputedCacheDataset(args.cache_dir)
|
| 153 |
+
print(f" {len(dataset)} cached samples")
|
| 154 |
+
loader = DataLoader(
|
| 155 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 156 |
+
num_workers=args.num_workers, collate_fn=ladd_collate, drop_last=True,
|
| 157 |
+
pin_memory=True,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# ----- training loop -----
|
| 161 |
+
print(f"[train] outer={args.total_outer_steps} "
|
| 162 |
+
f"cycle=({args.n_critic_per_gen} disc + 1 gen) "
|
| 163 |
+
f"n_student_steps={args.n_student_steps} dt={args.dt_ratio:.4f}")
|
| 164 |
+
log_path = out_dir / "dmdx_log.jsonl"
|
| 165 |
+
log_f = open(log_path, "a", buffering=1)
|
| 166 |
+
t0 = time.time()
|
| 167 |
+
data_iter = iter(loader)
|
| 168 |
+
|
| 169 |
+
def _next():
|
| 170 |
+
nonlocal data_iter
|
| 171 |
+
try:
|
| 172 |
+
return next(data_iter)
|
| 173 |
+
except StopIteration:
|
| 174 |
+
data_iter = iter(loader)
|
| 175 |
+
return next(data_iter)
|
| 176 |
+
|
| 177 |
+
def _prep_batch():
|
| 178 |
+
batch = _next()
|
| 179 |
+
x0_teacher = batch["x0"].to(device=device, dtype=dtype)
|
| 180 |
+
cond_pos = batch["emb"].to(device=device, dtype=dtype)
|
| 181 |
+
B = x0_teacher.size(0)
|
| 182 |
+
cond_neg_b = cond_neg.expand(B, -1, -1).contiguous() if B > 1 else cond_neg
|
| 183 |
+
noise = torch.randn_like(x0_teacher)
|
| 184 |
+
return noise, x0_teacher, cond_pos, cond_neg_b
|
| 185 |
+
|
| 186 |
+
for outer in range(args.total_outer_steps):
|
| 187 |
+
# ---- N disc updates ----
|
| 188 |
+
disc_metrics = {}
|
| 189 |
+
for _ in range(args.n_critic_per_gen):
|
| 190 |
+
noise, x0_teacher, cond_pos, cond_neg_b = _prep_batch()
|
| 191 |
+
disc.train()
|
| 192 |
+
student_transformer.eval()
|
| 193 |
+
opt_disc.zero_grad()
|
| 194 |
+
loss_d, m_d = adm_discriminator_loss(
|
| 195 |
+
student_v, teacher_v, disc,
|
| 196 |
+
noise, x0_teacher, cond_pos, cond_neg_b,
|
| 197 |
+
teacher_cfg=args.teacher_cfg, student_cfg=args.student_cfg,
|
| 198 |
+
n_student_steps=args.n_student_steps, dt_ratio=args.dt_ratio,
|
| 199 |
+
)
|
| 200 |
+
loss_d.backward()
|
| 201 |
+
torch.nn.utils.clip_grad_norm_(disc_params, args.grad_clip)
|
| 202 |
+
opt_disc.step()
|
| 203 |
+
disc_metrics = {k: float(v) for k, v in m_d.items()}
|
| 204 |
+
|
| 205 |
+
# ---- 1 generator update ----
|
| 206 |
+
noise, x0_teacher, cond_pos, cond_neg_b = _prep_batch()
|
| 207 |
+
student_transformer.train()
|
| 208 |
+
disc.eval()
|
| 209 |
+
opt_gen.zero_grad()
|
| 210 |
+
loss_g, m_g = adm_generator_loss(
|
| 211 |
+
student_v, teacher_v, disc,
|
| 212 |
+
noise, cond_pos, cond_neg_b,
|
| 213 |
+
real_x0=x0_teacher,
|
| 214 |
+
teacher_cfg=args.teacher_cfg, student_cfg=args.student_cfg,
|
| 215 |
+
n_student_steps=args.n_student_steps, dt_ratio=args.dt_ratio,
|
| 216 |
+
recon_weight=args.recon_weight,
|
| 217 |
+
)
|
| 218 |
+
loss_g.backward()
|
| 219 |
+
torch.nn.utils.clip_grad_norm_(student_params, args.grad_clip)
|
| 220 |
+
opt_gen.step()
|
| 221 |
+
gen_metrics = {k: float(v) for k, v in m_g.items()}
|
| 222 |
+
|
| 223 |
+
# ---- log ----
|
| 224 |
+
if outer % args.log_every == 0:
|
| 225 |
+
metrics = {"outer": outer, "elapsed": time.time() - t0,
|
| 226 |
+
**disc_metrics, **gen_metrics}
|
| 227 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 228 |
+
msg = " ".join(f"{k}={v:.4f}" for k, v in metrics.items() if k != "outer")
|
| 229 |
+
print(f"[outer {outer}/{args.total_outer_steps}] {msg}", flush=True)
|
| 230 |
+
|
| 231 |
+
# ---- ckpt (student LoRA only) ----
|
| 232 |
+
if outer > 0 and outer % args.sample_every == 0:
|
| 233 |
+
save_lora_state(student_transformer, out_dir, f"dmdx_student_step{outer:05d}")
|
| 234 |
+
print(f"[save] dmdx_student_step{outer:05d}.safetensors", flush=True)
|
| 235 |
+
try:
|
| 236 |
+
import modal
|
| 237 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 238 |
+
except Exception as e:
|
| 239 |
+
print(f"[save] volume commit failed: {e}", flush=True)
|
| 240 |
+
|
| 241 |
+
# final
|
| 242 |
+
print("[done] saving final")
|
| 243 |
+
save_lora_state(student_transformer, out_dir, "dmdx_student_final")
|
| 244 |
+
log_f.close()
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
if __name__ == "__main__":
|
| 248 |
+
main()
|
scripts/distill/train_draftp.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima DRaFT+ / AlignProp training with HPSv2 reward
|
| 4 |
+
====================================================
|
| 5 |
+
|
| 6 |
+
これは **品質向上** (速度向上ではない) 用の蒸留 LoRA fine-tuning。
|
| 7 |
+
既に蒸留された student LoRA を warm-start として、HPSv2 (Human Preference Score)
|
| 8 |
+
を maximize する方向に追加学習する。
|
| 9 |
+
|
| 10 |
+
Algorithm (DRaFT-K LV with KL regularization):
|
| 11 |
+
1. caption → cond_pos (no_grad)
|
| 12 |
+
2. init noise → student で N step rollout
|
| 13 |
+
- 前 N-K step: no_grad
|
| 14 |
+
- 後 K step: grad on (K=1 が paper の best)
|
| 15 |
+
3. final x0 → VAE decode (grad on) → image
|
| 16 |
+
4. reward = HPSv2(image, prompt)
|
| 17 |
+
5. KL term: ||v_pred - v_pred_base||² (LoRA disable して frozen base v_pred と比較)
|
| 18 |
+
6. loss = -reward + kl_coeff * KL
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
import argparse
|
| 22 |
+
import copy
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import sys
|
| 26 |
+
import time
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
from torch.utils.data import DataLoader
|
| 32 |
+
|
| 33 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 34 |
+
from distill.anima_loader import build_anima, AnimaBundle
|
| 35 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 36 |
+
from distill.train_traj import (
|
| 37 |
+
TextOnlyDataset, text_collate, load_warm_lora, save_lora_state,
|
| 38 |
+
)
|
| 39 |
+
from distill.traj_scheduler import make_schedule
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def vae_decode_with_grad(bundle: AnimaBundle, latents: torch.Tensor) -> torch.Tensor:
|
| 43 |
+
"""grad を通す VAE decode (anima_loader の vae_decode は @no_grad なので別実装)。"""
|
| 44 |
+
vae_dtype = next(bundle.vae.model.parameters()).dtype
|
| 45 |
+
latents = latents.to(dtype=vae_dtype)
|
| 46 |
+
return bundle.vae.model.decode(latents, bundle.vae_scale)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def student_rollout_with_truncation(
|
| 50 |
+
student_v_fn,
|
| 51 |
+
base_v_fn_for_kl,
|
| 52 |
+
init_noise: torch.Tensor,
|
| 53 |
+
schedule_ts: torch.Tensor, # (N+1,)
|
| 54 |
+
cond_pos: torch.Tensor,
|
| 55 |
+
student_cfg: float,
|
| 56 |
+
K: int,
|
| 57 |
+
capture_kl: bool = True,
|
| 58 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 59 |
+
"""rollout、最後の K step で grad on。KL 項用に最後の v_pred も別途 base で評価。
|
| 60 |
+
|
| 61 |
+
Returns: (x0_final, kl_loss)
|
| 62 |
+
"""
|
| 63 |
+
B = init_noise.size(0)
|
| 64 |
+
device = init_noise.device
|
| 65 |
+
dtype = init_noise.dtype
|
| 66 |
+
N = len(schedule_ts) - 1
|
| 67 |
+
truncate_idx = N - K
|
| 68 |
+
|
| 69 |
+
x = init_noise
|
| 70 |
+
kl_loss = torch.zeros((), device=device)
|
| 71 |
+
|
| 72 |
+
for i in range(N):
|
| 73 |
+
t_cur = schedule_ts[i].expand(B).to(dtype=dtype)
|
| 74 |
+
t_next = schedule_ts[i + 1]
|
| 75 |
+
is_grad_step = (i >= truncate_idx)
|
| 76 |
+
ctx = torch.enable_grad() if is_grad_step else torch.no_grad()
|
| 77 |
+
with ctx:
|
| 78 |
+
v = student_v_fn(x, t_cur, cond_pos)
|
| 79 |
+
if is_grad_step and capture_kl and i == N - 1:
|
| 80 |
+
# 最後の step で KL term: student LoRA v vs frozen base v
|
| 81 |
+
with torch.no_grad():
|
| 82 |
+
v_base = base_v_fn_for_kl(x, t_cur, cond_pos)
|
| 83 |
+
kl_loss = ((v - v_base.detach()).float() ** 2).mean()
|
| 84 |
+
dt = (t_next - schedule_ts[i]).to(device=device, dtype=dtype)
|
| 85 |
+
x = x + dt * v
|
| 86 |
+
return x, kl_loss
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def main():
|
| 90 |
+
ap = argparse.ArgumentParser()
|
| 91 |
+
ap.add_argument("--dataset", required=True, type=str,
|
| 92 |
+
help="caption-only dir (画像不要)")
|
| 93 |
+
ap.add_argument("--out", required=True, type=str)
|
| 94 |
+
ap.add_argument("--warm-lora", required=True, type=str,
|
| 95 |
+
help="必須: 既に蒸留された student LoRA (例 ① Z-Image の出力)")
|
| 96 |
+
ap.add_argument("--hps-weights", default="/models/hpsv2/HPS_v2_compressed.pt",
|
| 97 |
+
help="HPSv2 weights path、無ければ OpenCLIP baseline fallback")
|
| 98 |
+
ap.add_argument("--total-steps", type=int, default=1500)
|
| 99 |
+
ap.add_argument("--batch-size", type=int, default=2)
|
| 100 |
+
ap.add_argument("--grad-accum", type=int, default=1)
|
| 101 |
+
ap.add_argument("--n-student-steps", type=int, default=8)
|
| 102 |
+
ap.add_argument("--K", type=int, default=1, help="gradient truncation depth, paper best=1")
|
| 103 |
+
ap.add_argument("--n-lv-samples", type=int, default=2,
|
| 104 |
+
help="DRaFT-LV: extra noise samples at last step, averaged")
|
| 105 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 106 |
+
ap.add_argument("--student-cfg", type=float, default=1.0)
|
| 107 |
+
ap.add_argument("--sigma-shift", type=float, default=3.0)
|
| 108 |
+
ap.add_argument("--lr", type=float, default=1e-4)
|
| 109 |
+
ap.add_argument("--kl-coeff", type=float, default=0.2,
|
| 110 |
+
help="NeMo DRaFT+ default、reward hacking 防止")
|
| 111 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 112 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 113 |
+
ap.add_argument("--log-every", type=int, default=5)
|
| 114 |
+
ap.add_argument("--sample-every", type=int, default=200)
|
| 115 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 116 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 117 |
+
args = ap.parse_args()
|
| 118 |
+
|
| 119 |
+
torch.manual_seed(args.seed)
|
| 120 |
+
device = torch.device("cuda")
|
| 121 |
+
dtype = torch.bfloat16
|
| 122 |
+
out_dir = Path(args.out)
|
| 123 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 124 |
+
|
| 125 |
+
print("[load] Anima bundle")
|
| 126 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 127 |
+
|
| 128 |
+
# base = frozen deepcopy (KL term の reference として使う)
|
| 129 |
+
print("[setup] base = frozen deepcopy (for KL reference)")
|
| 130 |
+
base_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype).eval()
|
| 131 |
+
for p in base_transformer.parameters():
|
| 132 |
+
p.requires_grad = False
|
| 133 |
+
|
| 134 |
+
# student = wide LoRA
|
| 135 |
+
student_transformer = attach_wide_lora(bundle.transformer, rank=args.lora_rank)
|
| 136 |
+
student_transformer.to(device=device, dtype=dtype)
|
| 137 |
+
for n, p in student_transformer.named_parameters():
|
| 138 |
+
p.requires_grad = ("lora_" in n)
|
| 139 |
+
student_params = [p for p in student_transformer.parameters() if p.requires_grad]
|
| 140 |
+
print(f"[setup] student trainable: {sum(p.numel() for p in student_params)/1e6:.1f}M")
|
| 141 |
+
bundle.transformer = student_transformer
|
| 142 |
+
|
| 143 |
+
# warm-start 必須
|
| 144 |
+
load_warm_lora(student_transformer, args.warm_lora)
|
| 145 |
+
|
| 146 |
+
# HPSv2 reward
|
| 147 |
+
print(f"[setup] loading HPSv2 from {args.hps_weights}")
|
| 148 |
+
from distill.hps_reward import HPSv2Reward
|
| 149 |
+
hps = HPSv2Reward(args.hps_weights, device=device, dtype=torch.float32)
|
| 150 |
+
|
| 151 |
+
# schedule (固定 N step)
|
| 152 |
+
sched = make_schedule(args.n_student_steps, args.sigma_shift,
|
| 153 |
+
device=device, dtype=torch.float32)
|
| 154 |
+
print(f"[schedule] N={sched.num_steps} timesteps={sched.timesteps.tolist()}")
|
| 155 |
+
|
| 156 |
+
# dataset (caption-only)
|
| 157 |
+
print(f"[data] {args.dataset}")
|
| 158 |
+
dataset = TextOnlyDataset(args.dataset)
|
| 159 |
+
print(f" {len(dataset)} captions")
|
| 160 |
+
loader = DataLoader(
|
| 161 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 162 |
+
num_workers=args.num_workers, collate_fn=text_collate,
|
| 163 |
+
drop_last=True,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
opt = torch.optim.AdamW(student_params, lr=args.lr, betas=(0.9, 0.999),
|
| 167 |
+
weight_decay=0.01, eps=1e-8)
|
| 168 |
+
|
| 169 |
+
def student_v_fn(x, t, cond):
|
| 170 |
+
return AnimaBundle.dit_forward(student_transformer, x, t, cond)
|
| 171 |
+
|
| 172 |
+
def base_v_fn(x, t, cond):
|
| 173 |
+
return AnimaBundle.dit_forward(base_transformer, x, t, cond)
|
| 174 |
+
|
| 175 |
+
H_lat = args.resolution // 8
|
| 176 |
+
W_lat = args.resolution // 8
|
| 177 |
+
|
| 178 |
+
print(f"[train] steps={args.total_steps} bs={args.batch_size} N={args.n_student_steps} "
|
| 179 |
+
f"K={args.K} lv={args.n_lv_samples} kl={args.kl_coeff}")
|
| 180 |
+
log_path = out_dir / "draftp_log.jsonl"
|
| 181 |
+
log_f = open(log_path, "a", buffering=1)
|
| 182 |
+
t0 = time.time()
|
| 183 |
+
data_iter = iter(loader)
|
| 184 |
+
|
| 185 |
+
def _next():
|
| 186 |
+
nonlocal data_iter
|
| 187 |
+
try:
|
| 188 |
+
return next(data_iter)
|
| 189 |
+
except StopIteration:
|
| 190 |
+
data_iter = iter(loader)
|
| 191 |
+
return next(data_iter)
|
| 192 |
+
|
| 193 |
+
for step in range(args.total_steps):
|
| 194 |
+
student_transformer.train()
|
| 195 |
+
opt.zero_grad()
|
| 196 |
+
metrics = {}
|
| 197 |
+
for _ in range(args.grad_accum):
|
| 198 |
+
captions = _next()
|
| 199 |
+
with torch.no_grad():
|
| 200 |
+
cond_pos = bundle.text_encode(captions)
|
| 201 |
+
B = cond_pos.size(0)
|
| 202 |
+
init_noise = torch.randn(B, 16, 1, H_lat, W_lat, device=device, dtype=dtype)
|
| 203 |
+
|
| 204 |
+
# DRaFT-LV: rollout once + n_lv_samples 個の last-step alternative を試して平均
|
| 205 |
+
x0_final, kl = student_rollout_with_truncation(
|
| 206 |
+
student_v_fn, base_v_fn, init_noise, sched.timesteps,
|
| 207 |
+
cond_pos, args.student_cfg, args.K, capture_kl=True,
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
# VAE decode (grad on)
|
| 211 |
+
img = vae_decode_with_grad(bundle, x0_final).squeeze(2) # (B,3,H,W) in [-1,1]
|
| 212 |
+
reward = hps.score(img, captions) # (B,)
|
| 213 |
+
r_mean = reward.mean()
|
| 214 |
+
|
| 215 |
+
# DRaFT+ loss: -reward + kl_coeff * kl
|
| 216 |
+
loss = (-r_mean + args.kl_coeff * kl) / args.grad_accum
|
| 217 |
+
loss.backward()
|
| 218 |
+
metrics = {
|
| 219 |
+
"reward_mean": float(r_mean.detach()),
|
| 220 |
+
"reward_std": float(reward.std().detach()),
|
| 221 |
+
"kl": float(kl.detach()),
|
| 222 |
+
"loss": float((-r_mean + args.kl_coeff * kl).detach()),
|
| 223 |
+
}
|
| 224 |
+
torch.nn.utils.clip_grad_norm_(student_params, args.grad_clip)
|
| 225 |
+
opt.step()
|
| 226 |
+
|
| 227 |
+
if step % args.log_every == 0:
|
| 228 |
+
metrics["step"] = step
|
| 229 |
+
metrics["elapsed"] = time.time() - t0
|
| 230 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 231 |
+
msg = " ".join(f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}"
|
| 232 |
+
for k, v in metrics.items() if k != "step")
|
| 233 |
+
print(f"[step {step}/{args.total_steps}] {msg}", flush=True)
|
| 234 |
+
|
| 235 |
+
if step > 0 and step % args.sample_every == 0:
|
| 236 |
+
save_lora_state(student_transformer, out_dir, f"draftp_step{step:05d}")
|
| 237 |
+
print(f"[save] draftp_step{step:05d}.safetensors", flush=True)
|
| 238 |
+
try:
|
| 239 |
+
import modal
|
| 240 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 241 |
+
except Exception:
|
| 242 |
+
pass
|
| 243 |
+
|
| 244 |
+
print("[done] saving final")
|
| 245 |
+
save_lora_state(student_transformer, out_dir, "draftp_final")
|
| 246 |
+
log_f.close()
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
main()
|
scripts/distill/train_ladd.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima LADD (Latent Adversarial Diffusion Distillation) — AMD Nitro-1 移植
|
| 4 |
+
|
| 5 |
+
R3GAN 失敗との対比:
|
| 6 |
+
- R3GAN: CNN-on-latent を一から学習 → Anima 16ch latent prior なし → 崩壊
|
| 7 |
+
- LADD: D の backbone = teacher MiniTrainDIT (frozen)、head だけ trainable
|
| 8 |
+
+ Smooth-L1 recon anchor で mean collapse の引力を断つ
|
| 9 |
+
→ R3GAN の "stable zone too narrow" を構造的に回避
|
| 10 |
+
|
| 11 |
+
訓練ループ (1 step per phase, alternate G:D = 1:1):
|
| 12 |
+
G phase:
|
| 13 |
+
1. student で 1-step rollout (t=1 → t=0)、x0_hat 取得
|
| 14 |
+
2. x0_hat を t_D ∈ [0, 0.75] で re-noise
|
| 15 |
+
3. D で logits_fake、adv_loss = BCE(logits_fake, 1)
|
| 16 |
+
4. recon_loss = smooth_L1(x0_hat, x0_teacher_cached)
|
| 17 |
+
5. G_loss = adv_loss + recon_lambda * recon_loss
|
| 18 |
+
|
| 19 |
+
D phase:
|
| 20 |
+
1. student で 1-step rollout (no_grad)、x0_hat
|
| 21 |
+
2. teacher x0 (cached) と x0_hat をそれぞれ別の t_D で re-noise
|
| 22 |
+
3. D で logits_real / logits_fake、BCE(real, 1) + BCE(fake, 0)
|
| 23 |
+
|
| 24 |
+
precompute 必須:
|
| 25 |
+
modal run modal_app.py::precompute_teacher_x0_cache
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
import argparse
|
| 29 |
+
import copy
|
| 30 |
+
import json
|
| 31 |
+
import os
|
| 32 |
+
import sys
|
| 33 |
+
import time
|
| 34 |
+
from pathlib import Path
|
| 35 |
+
|
| 36 |
+
import torch
|
| 37 |
+
import torch.nn.functional as F
|
| 38 |
+
from torch.utils.data import DataLoader, Dataset
|
| 39 |
+
from safetensors.torch import save_file, load_file
|
| 40 |
+
|
| 41 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 42 |
+
from distill.anima_loader import AnimaPaths, build_anima, AnimaBundle
|
| 43 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 44 |
+
from distill.train_traj import convert_comfy_to_peft_lora, save_lora_state
|
| 45 |
+
from distill.dmd2_official_loss import renoise_rf, x0_from_velocity_rf
|
| 46 |
+
from distill.anima_ladd_disc import (
|
| 47 |
+
AnimaLADDDiscriminator, ladd_d_loss, ladd_g_adv_loss, ladd_g_recon_loss,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ----- precomputed cache dataset -------------------------------------------
|
| 52 |
+
class PrecomputedCacheDataset(Dataset):
|
| 53 |
+
"""teacher_x0_cache から (caption_emb, teacher_x0) を読む。"""
|
| 54 |
+
def __init__(self, cache_dir: str | Path):
|
| 55 |
+
self.cache_dir = Path(cache_dir)
|
| 56 |
+
meta_path = self.cache_dir / "metadata.json"
|
| 57 |
+
self.meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
| 58 |
+
if len(self.meta) == 0:
|
| 59 |
+
raise RuntimeError(f"Empty metadata at {meta_path}")
|
| 60 |
+
|
| 61 |
+
def __len__(self):
|
| 62 |
+
return len(self.meta)
|
| 63 |
+
|
| 64 |
+
def __getitem__(self, idx):
|
| 65 |
+
m = self.meta[idx]
|
| 66 |
+
x0 = torch.load(m["x0_path"], map_location="cpu", weights_only=True)
|
| 67 |
+
emb = torch.load(m["emb_path"], map_location="cpu", weights_only=True)
|
| 68 |
+
# x0 was saved as (1, 16, 1, H, W), emb as (1, 512, 1024) → squeeze batch dim
|
| 69 |
+
return {"x0": x0.squeeze(0), "emb": emb.squeeze(0), "caption": m["caption"]}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def ladd_collate(batch):
|
| 73 |
+
x0 = torch.stack([b["x0"] for b in batch])
|
| 74 |
+
emb = torch.stack([b["emb"] for b in batch])
|
| 75 |
+
captions = [b["caption"] for b in batch]
|
| 76 |
+
return {"x0": x0, "emb": emb, "captions": captions}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ----- student 1-step rollout ----------------------------------------------
|
| 80 |
+
def student_x0_hat(
|
| 81 |
+
student_v_fn, noise: torch.Tensor, cond_pos: torch.Tensor,
|
| 82 |
+
) -> torch.Tensor:
|
| 83 |
+
"""1-step distill: t=1 noise から 1 step Euler で t=0 へ。"""
|
| 84 |
+
B = noise.size(0)
|
| 85 |
+
device = noise.device
|
| 86 |
+
dtype = noise.dtype
|
| 87 |
+
t_init = torch.ones(B, device=device, dtype=dtype) # t=1
|
| 88 |
+
v = student_v_fn(noise, t_init, cond_pos)
|
| 89 |
+
# x0 = x_t - t * v = noise - 1 * v
|
| 90 |
+
return noise - v
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ---------------------------------------------------------------------------
|
| 94 |
+
def main():
|
| 95 |
+
ap = argparse.ArgumentParser()
|
| 96 |
+
ap.add_argument("--cache-dir", required=True, type=str)
|
| 97 |
+
ap.add_argument("--out", required=True, type=str)
|
| 98 |
+
ap.add_argument("--warm-lora", default="", type=str)
|
| 99 |
+
ap.add_argument("--total-steps", type=int, default=5000)
|
| 100 |
+
ap.add_argument("--batch-size", type=int, default=4)
|
| 101 |
+
ap.add_argument("--grad-accum", type=int, default=4, help="effective bs = batch_size * grad_accum")
|
| 102 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 103 |
+
ap.add_argument("--recon-lambda", type=float, default=1.0)
|
| 104 |
+
ap.add_argument("--lr-g", type=float, default=1e-6)
|
| 105 |
+
ap.add_argument("--lr-d", type=float, default=1e-6)
|
| 106 |
+
ap.add_argument("--t-d-max", type=float, default=0.75, help="re-noise t upper bound")
|
| 107 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 108 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 109 |
+
ap.add_argument("--block-ids", type=str, default="2,8,14,20,26",
|
| 110 |
+
help="teacher block indices for D hooks (Anima 28 blocks)")
|
| 111 |
+
ap.add_argument("--head-hidden", type=int, default=512)
|
| 112 |
+
ap.add_argument("--misaligned-pairs-d", action="store_true", default=True,
|
| 113 |
+
help="text alignment trick: D も misaligned fake (caption roll) を学習")
|
| 114 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 115 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 116 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 117 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 118 |
+
args = ap.parse_args()
|
| 119 |
+
|
| 120 |
+
torch.manual_seed(args.seed)
|
| 121 |
+
device = torch.device("cuda")
|
| 122 |
+
dtype = torch.bfloat16
|
| 123 |
+
out_dir = Path(args.out)
|
| 124 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 125 |
+
|
| 126 |
+
# ----- load Anima base -----
|
| 127 |
+
print("[load] Anima bundle")
|
| 128 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 129 |
+
|
| 130 |
+
# ----- teacher = deepcopy frozen (D の backbone と shared instance) -----
|
| 131 |
+
print("[setup] teacher = frozen deepcopy")
|
| 132 |
+
teacher_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype).eval()
|
| 133 |
+
for p in teacher_transformer.parameters():
|
| 134 |
+
p.requires_grad = False
|
| 135 |
+
|
| 136 |
+
# ----- student = wide LoRA -----
|
| 137 |
+
print("[setup] student = wide LoRA on bundle.transformer")
|
| 138 |
+
student_transformer = attach_wide_lora(bundle.transformer, rank=args.lora_rank)
|
| 139 |
+
student_transformer.to(device=device, dtype=dtype)
|
| 140 |
+
for n, p in student_transformer.named_parameters():
|
| 141 |
+
p.requires_grad = ("lora_" in n)
|
| 142 |
+
student_params = [p for p in student_transformer.parameters() if p.requires_grad]
|
| 143 |
+
print(f"[setup] student trainable: {sum(p.numel() for p in student_params)/1e6:.1f}M")
|
| 144 |
+
bundle.transformer = student_transformer
|
| 145 |
+
|
| 146 |
+
# warm-start
|
| 147 |
+
if args.warm_lora:
|
| 148 |
+
from distill.train_traj import load_warm_lora
|
| 149 |
+
load_warm_lora(student_transformer, args.warm_lora)
|
| 150 |
+
|
| 151 |
+
# ----- discriminator -----
|
| 152 |
+
print("[setup] LADD discriminator (teacher backbone + multi-scale heads)")
|
| 153 |
+
block_ids = [int(x) for x in args.block_ids.split(",")]
|
| 154 |
+
disc = AnimaLADDDiscriminator(
|
| 155 |
+
teacher_transformer=teacher_transformer,
|
| 156 |
+
block_ids=block_ids, head_hidden=args.head_hidden,
|
| 157 |
+
)
|
| 158 |
+
# heads を lazy init するために dummy forward
|
| 159 |
+
H_lat = args.resolution // 8
|
| 160 |
+
W_lat = args.resolution // 8
|
| 161 |
+
dummy_x = torch.randn(1, 16, 1, H_lat, W_lat, device=device, dtype=dtype)
|
| 162 |
+
dummy_t = torch.tensor([0.5], device=device, dtype=dtype)
|
| 163 |
+
with torch.no_grad():
|
| 164 |
+
dummy_cond = bundle.text_encode([""])
|
| 165 |
+
disc.lazy_init_heads(dummy_x, dummy_t, dummy_cond)
|
| 166 |
+
disc.to(device=device, dtype=torch.float32) # heads は fp32
|
| 167 |
+
disc_params = disc.trainable_parameters()
|
| 168 |
+
print(f"[setup] D heads trainable: {sum(p.numel() for p in disc_params)/1e6:.1f}M")
|
| 169 |
+
|
| 170 |
+
# ----- optimizers -----
|
| 171 |
+
opt_g = torch.optim.AdamW(student_params, lr=args.lr_g, betas=(0.0, 0.999), eps=1e-8)
|
| 172 |
+
opt_d = torch.optim.AdamW(disc_params, lr=args.lr_d, betas=(0.0, 0.999), eps=1e-8)
|
| 173 |
+
|
| 174 |
+
# ----- velocity functions -----
|
| 175 |
+
def student_v(x, t, cond):
|
| 176 |
+
return AnimaBundle.dit_forward(student_transformer, x, t, cond)
|
| 177 |
+
|
| 178 |
+
# ----- dataset (precomputed cache) -----
|
| 179 |
+
print(f"[data] loading cache {args.cache_dir}")
|
| 180 |
+
dataset = PrecomputedCacheDataset(args.cache_dir)
|
| 181 |
+
print(f" {len(dataset)} cached samples")
|
| 182 |
+
loader = DataLoader(
|
| 183 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 184 |
+
num_workers=args.num_workers, collate_fn=ladd_collate, drop_last=True,
|
| 185 |
+
pin_memory=True,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# ----- training loop -----
|
| 189 |
+
print(f"[train] total={args.total_steps} bs={args.batch_size} accum={args.grad_accum} "
|
| 190 |
+
f"recon_lambda={args.recon_lambda}")
|
| 191 |
+
log_path = out_dir / "ladd_log.jsonl"
|
| 192 |
+
log_f = open(log_path, "a", buffering=1)
|
| 193 |
+
t0 = time.time()
|
| 194 |
+
data_iter = iter(loader)
|
| 195 |
+
|
| 196 |
+
def _next():
|
| 197 |
+
nonlocal data_iter
|
| 198 |
+
try:
|
| 199 |
+
return next(data_iter)
|
| 200 |
+
except StopIteration:
|
| 201 |
+
data_iter = iter(loader)
|
| 202 |
+
return next(data_iter)
|
| 203 |
+
|
| 204 |
+
for step in range(args.total_steps):
|
| 205 |
+
# ---- G phase ----
|
| 206 |
+
student_transformer.train()
|
| 207 |
+
opt_g.zero_grad()
|
| 208 |
+
g_metrics = {}
|
| 209 |
+
for _ in range(args.grad_accum):
|
| 210 |
+
batch = _next()
|
| 211 |
+
x0_teacher = batch["x0"].to(device=device, dtype=dtype)
|
| 212 |
+
cond_pos = batch["emb"].to(device=device, dtype=dtype)
|
| 213 |
+
B = x0_teacher.size(0)
|
| 214 |
+
noise = torch.randn_like(x0_teacher)
|
| 215 |
+
# student forward
|
| 216 |
+
x0_hat = student_x0_hat(student_v, noise, cond_pos)
|
| 217 |
+
# re-noise & D forward (fake side)
|
| 218 |
+
t_D = torch.rand(B, device=device, dtype=dtype) * args.t_d_max
|
| 219 |
+
D_eps = torch.randn_like(x0_hat)
|
| 220 |
+
x_t_fake = renoise_rf(x0_hat, t_D, D_eps)
|
| 221 |
+
# G phase: adv loss が student を訓練するために gradient_to_input=True
|
| 222 |
+
logits_fake = disc(x_t_fake, t_D, cond_pos, gradient_to_input=True)
|
| 223 |
+
l_adv, m_adv = ladd_g_adv_loss(logits_fake)
|
| 224 |
+
l_rec, m_rec = ladd_g_recon_loss(x0_hat, x0_teacher)
|
| 225 |
+
g_loss = (l_adv + args.recon_lambda * l_rec) / args.grad_accum
|
| 226 |
+
g_loss.backward()
|
| 227 |
+
g_metrics = {**m_adv, **m_rec, "l_g_total": (l_adv + args.recon_lambda * l_rec).detach()}
|
| 228 |
+
torch.nn.utils.clip_grad_norm_(student_params, args.grad_clip)
|
| 229 |
+
opt_g.step()
|
| 230 |
+
|
| 231 |
+
# ---- D phase ----
|
| 232 |
+
disc.train()
|
| 233 |
+
opt_d.zero_grad()
|
| 234 |
+
d_metrics = {}
|
| 235 |
+
for _ in range(args.grad_accum):
|
| 236 |
+
batch = _next()
|
| 237 |
+
x0_teacher = batch["x0"].to(device=device, dtype=dtype)
|
| 238 |
+
cond_pos = batch["emb"].to(device=device, dtype=dtype)
|
| 239 |
+
B = x0_teacher.size(0)
|
| 240 |
+
with torch.no_grad():
|
| 241 |
+
noise = torch.randn_like(x0_teacher)
|
| 242 |
+
x0_hat = student_x0_hat(student_v, noise, cond_pos)
|
| 243 |
+
t_D_real = torch.rand(B, device=device, dtype=dtype) * args.t_d_max
|
| 244 |
+
t_D_fake = torch.rand(B, device=device, dtype=dtype) * args.t_d_max
|
| 245 |
+
x_t_real = renoise_rf(x0_teacher, t_D_real, torch.randn_like(x0_teacher))
|
| 246 |
+
x_t_fake = renoise_rf(x0_hat, t_D_fake, torch.randn_like(x0_hat))
|
| 247 |
+
cond_for_real = cond_pos
|
| 248 |
+
cond_for_fake = cond_pos
|
| 249 |
+
if args.misaligned_pairs_d:
|
| 250 |
+
# text-alignment: misaligned fake (caption を 1 個 roll) も追加
|
| 251 |
+
cond_misaligned = torch.roll(cond_pos, 1, dims=0)
|
| 252 |
+
t_D_mis = torch.rand(B, device=device, dtype=dtype) * args.t_d_max
|
| 253 |
+
x_t_mis = renoise_rf(x0_teacher, t_D_mis, torch.randn_like(x0_teacher))
|
| 254 |
+
x_t_fake = torch.cat([x_t_fake, x_t_mis], dim=0)
|
| 255 |
+
t_D_fake = torch.cat([t_D_fake, t_D_mis], dim=0)
|
| 256 |
+
cond_for_fake = torch.cat([cond_pos, cond_misaligned], dim=0)
|
| 257 |
+
# D phase: heads だけ訓練、x への gradient 不要 (gradient_to_input=False、省メモリ)
|
| 258 |
+
logits_real = disc(x_t_real, t_D_real, cond_for_real, gradient_to_input=False)
|
| 259 |
+
logits_fake = disc(x_t_fake, t_D_fake, cond_for_fake, gradient_to_input=False)
|
| 260 |
+
d_loss, m_d = ladd_d_loss(logits_real, logits_fake)
|
| 261 |
+
d_loss = d_loss / args.grad_accum
|
| 262 |
+
d_loss.backward()
|
| 263 |
+
d_metrics = {k: float(v) for k, v in m_d.items()}
|
| 264 |
+
torch.nn.utils.clip_grad_norm_(disc_params, args.grad_clip)
|
| 265 |
+
opt_d.step()
|
| 266 |
+
|
| 267 |
+
# ---- log ----
|
| 268 |
+
if step % args.log_every == 0:
|
| 269 |
+
metrics = {"step": step, "elapsed": time.time() - t0,
|
| 270 |
+
**{k: float(v) for k, v in g_metrics.items()},
|
| 271 |
+
**d_metrics}
|
| 272 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 273 |
+
msg = " ".join(f"{k}={v:.4f}" for k, v in metrics.items() if k not in ("step",))
|
| 274 |
+
print(f"[step {step}/{args.total_steps}] {msg}", flush=True)
|
| 275 |
+
|
| 276 |
+
# ---- ckpt ----
|
| 277 |
+
if step > 0 and step % args.sample_every == 0:
|
| 278 |
+
save_lora_state(student_transformer, out_dir, f"ladd_student_step{step:05d}")
|
| 279 |
+
print(f"[save] ladd_student_step{step:05d}.safetensors", flush=True)
|
| 280 |
+
try:
|
| 281 |
+
import modal
|
| 282 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 283 |
+
except Exception:
|
| 284 |
+
pass
|
| 285 |
+
|
| 286 |
+
# final
|
| 287 |
+
print("[done] saving final")
|
| 288 |
+
save_lora_state(student_transformer, out_dir, "ladd_student_final")
|
| 289 |
+
log_f.close()
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
if __name__ == "__main__":
|
| 293 |
+
main()
|
scripts/distill/train_pcm.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima PCM (Phased Consistency Model) distillation
|
| 4 |
+
=================================================
|
| 5 |
+
|
| 6 |
+
Reference: G-U-N/Phased-Consistency-Model
|
| 7 |
+
code/text_to_image_sd3/train_pcm_lora_sd3.py
|
| 8 |
+
|
| 9 |
+
SD3 PCM は FlowMatch + v-pred + LoRA で、Anima の rectified flow と math 完全一致。
|
| 10 |
+
ε↔v 変換不要、scheduler/loss を直接移植可能。
|
| 11 |
+
|
| 12 |
+
Algorithm:
|
| 13 |
+
- num_euler_timesteps=N (e.g. 50) で grid を作り、K phase に等分割
|
| 14 |
+
- 各 step ランダムに index in [0, N) を選び、現 phase の終端まで Euler 1 step
|
| 15 |
+
- student の predicted x_phase と、teacher 1 step 前進 → student EMA-target の x_phase を MSE
|
| 16 |
+
- pseudo-Huber loss + 任意 adv loss (skip in v1)
|
| 17 |
+
|
| 18 |
+
memory: 1 grad-through student fwd + 3 no_grad fwd (teacher cond, teacher uncond, student-as-EMA)
|
| 19 |
+
→ ~60-80 GB on B200 (LoRA-only, batch=1, 768²)
|
| 20 |
+
|
| 21 |
+
データ: LADD precompute cache の emb を流用 (prompt 再 encode 不要)
|
| 22 |
+
x0 は teacher rollout で online 生成するので不要 (ただし noise / cap 起点として cache を使う)
|
| 23 |
+
"""
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
import argparse
|
| 26 |
+
import copy
|
| 27 |
+
import json
|
| 28 |
+
import os
|
| 29 |
+
import sys
|
| 30 |
+
import time
|
| 31 |
+
from pathlib import Path
|
| 32 |
+
|
| 33 |
+
import torch
|
| 34 |
+
import torch.nn.functional as F
|
| 35 |
+
from torch.utils.data import DataLoader, Dataset
|
| 36 |
+
from safetensors.torch import save_file, load_file
|
| 37 |
+
|
| 38 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 39 |
+
from distill.anima_loader import AnimaPaths, build_anima, AnimaBundle
|
| 40 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 41 |
+
from distill.train_traj import load_warm_lora, save_lora_state
|
| 42 |
+
from distill.pcm_scheduler import make_pcm_solver, euler_multiphase, pseudo_huber
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ----- dataset: precomputed cache の emb と任意で x0 を使う ----------------
|
| 46 |
+
class PCMCacheDataset(Dataset):
|
| 47 |
+
"""LADD/Reflow cache の (caption, emb, x0) を読む。x0 は initial noise の source。"""
|
| 48 |
+
def __init__(self, cache_dir: str | Path):
|
| 49 |
+
self.cache_dir = Path(cache_dir)
|
| 50 |
+
self.meta = json.loads((self.cache_dir / "metadata.json").read_text(encoding="utf-8"))
|
| 51 |
+
|
| 52 |
+
def __len__(self):
|
| 53 |
+
return len(self.meta)
|
| 54 |
+
|
| 55 |
+
def __getitem__(self, idx):
|
| 56 |
+
m = self.meta[idx]
|
| 57 |
+
x0 = torch.load(m["x0_path"], map_location="cpu", weights_only=True).squeeze(0)
|
| 58 |
+
emb = torch.load(m["emb_path"], map_location="cpu", weights_only=True).squeeze(0)
|
| 59 |
+
return {"x0": x0, "emb": emb}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def pcm_collate(batch):
|
| 63 |
+
return {
|
| 64 |
+
"x0": torch.stack([b["x0"] for b in batch]),
|
| 65 |
+
"emb": torch.stack([b["emb"] for b in batch]),
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def main():
|
| 70 |
+
ap = argparse.ArgumentParser()
|
| 71 |
+
ap.add_argument("--cache-dir", required=True, type=str,
|
| 72 |
+
help="LADD cache (emb と x0 を使用)")
|
| 73 |
+
ap.add_argument("--out", required=True, type=str)
|
| 74 |
+
ap.add_argument("--warm-lora", default="", type=str)
|
| 75 |
+
ap.add_argument("--total-steps", type=int, default=8000)
|
| 76 |
+
ap.add_argument("--batch-size", type=int, default=1)
|
| 77 |
+
ap.add_argument("--grad-accum", type=int, default=4)
|
| 78 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 79 |
+
ap.add_argument("--num-euler-timesteps", type=int, default=50)
|
| 80 |
+
ap.add_argument("--num-phases", type=int, default=4)
|
| 81 |
+
ap.add_argument("--sigma-shift", type=float, default=3.0)
|
| 82 |
+
ap.add_argument("--w-min", type=float, default=4.0, help="CFG-aug min")
|
| 83 |
+
ap.add_argument("--w-max", type=float, default=5.0, help="CFG-aug max")
|
| 84 |
+
ap.add_argument("--w-fixed", type=float, default=-1.0, help=">=0 で範囲指定無視して固定")
|
| 85 |
+
ap.add_argument("--huber-c", type=float, default=1e-3)
|
| 86 |
+
ap.add_argument("--lr", type=float, default=5e-6)
|
| 87 |
+
ap.add_argument("--weight-decay", type=float, default=0.01)
|
| 88 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 89 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 90 |
+
ap.add_argument("--neg-prompt", default="")
|
| 91 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 92 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 93 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 94 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 95 |
+
args = ap.parse_args()
|
| 96 |
+
|
| 97 |
+
torch.manual_seed(args.seed)
|
| 98 |
+
device = torch.device("cuda")
|
| 99 |
+
dtype = torch.bfloat16
|
| 100 |
+
out_dir = Path(args.out)
|
| 101 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
|
| 103 |
+
# ----- load Anima -----
|
| 104 |
+
print("[load] Anima bundle")
|
| 105 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 106 |
+
|
| 107 |
+
# teacher = frozen deepcopy
|
| 108 |
+
print("[setup] teacher = frozen deepcopy")
|
| 109 |
+
teacher_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype).eval()
|
| 110 |
+
for p in teacher_transformer.parameters():
|
| 111 |
+
p.requires_grad = False
|
| 112 |
+
|
| 113 |
+
# student = wide LoRA
|
| 114 |
+
student_transformer = attach_wide_lora(bundle.transformer, rank=args.lora_rank)
|
| 115 |
+
student_transformer.to(device=device, dtype=dtype)
|
| 116 |
+
for n, p in student_transformer.named_parameters():
|
| 117 |
+
p.requires_grad = ("lora_" in n)
|
| 118 |
+
student_params = [p for p in student_transformer.parameters() if p.requires_grad]
|
| 119 |
+
print(f"[setup] student trainable: {sum(p.numel() for p in student_params)/1e6:.1f}M")
|
| 120 |
+
bundle.transformer = student_transformer
|
| 121 |
+
|
| 122 |
+
if args.warm_lora:
|
| 123 |
+
load_warm_lora(student_transformer, args.warm_lora)
|
| 124 |
+
|
| 125 |
+
# solver
|
| 126 |
+
solver = make_pcm_solver(
|
| 127 |
+
num_euler_timesteps=args.num_euler_timesteps,
|
| 128 |
+
num_phases=args.num_phases,
|
| 129 |
+
sigma_shift=args.sigma_shift,
|
| 130 |
+
device=device, dtype=torch.float32,
|
| 131 |
+
)
|
| 132 |
+
print(f"[schedule] N={solver.num_steps} K={len(solver.phase_ends)} "
|
| 133 |
+
f"phase_ends={solver.phase_ends.tolist()}")
|
| 134 |
+
|
| 135 |
+
# cond_neg
|
| 136 |
+
with torch.no_grad():
|
| 137 |
+
cond_neg = bundle.text_encode([args.neg_prompt or ""])
|
| 138 |
+
|
| 139 |
+
# optimizer
|
| 140 |
+
opt = torch.optim.AdamW(student_params, lr=args.lr, betas=(0.9, 0.999),
|
| 141 |
+
weight_decay=args.weight_decay, eps=1e-8)
|
| 142 |
+
|
| 143 |
+
# dataset (emb は cache から、x0 は initial noise の source として使う —
|
| 144 |
+
# 実際の noise は毎 step randn)
|
| 145 |
+
print(f"[data] {args.cache_dir}")
|
| 146 |
+
dataset = PCMCacheDataset(args.cache_dir)
|
| 147 |
+
print(f" {len(dataset)} captions")
|
| 148 |
+
loader = DataLoader(
|
| 149 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 150 |
+
num_workers=args.num_workers, collate_fn=pcm_collate,
|
| 151 |
+
drop_last=True, pin_memory=True,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
print(f"[train] steps={args.total_steps} bs={args.batch_size} accum={args.grad_accum} "
|
| 155 |
+
f"N={args.num_euler_timesteps} K={args.num_phases}")
|
| 156 |
+
log_path = out_dir / "pcm_log.jsonl"
|
| 157 |
+
log_f = open(log_path, "a", buffering=1)
|
| 158 |
+
t0 = time.time()
|
| 159 |
+
data_iter = iter(loader)
|
| 160 |
+
|
| 161 |
+
def _next():
|
| 162 |
+
nonlocal data_iter
|
| 163 |
+
try:
|
| 164 |
+
return next(data_iter)
|
| 165 |
+
except StopIteration:
|
| 166 |
+
data_iter = iter(loader)
|
| 167 |
+
return next(data_iter)
|
| 168 |
+
|
| 169 |
+
for step in range(args.total_steps):
|
| 170 |
+
student_transformer.train()
|
| 171 |
+
opt.zero_grad()
|
| 172 |
+
metrics = {}
|
| 173 |
+
for _ in range(args.grad_accum):
|
| 174 |
+
batch = _next()
|
| 175 |
+
x0 = batch["x0"].to(device=device, dtype=dtype)
|
| 176 |
+
emb = batch["emb"].to(device=device, dtype=dtype)
|
| 177 |
+
B = x0.size(0)
|
| 178 |
+
cond_neg_b = cond_neg.expand(B, -1, -1).contiguous() if B > 1 else cond_neg
|
| 179 |
+
|
| 180 |
+
# ----- index sampling -----
|
| 181 |
+
index = torch.randint(0, solver.num_steps, (B,), device=device)
|
| 182 |
+
sigma_cur = solver.sigmas[index].to(dtype=dtype)
|
| 183 |
+
sigma_prev = solver.sigmas[index + 1].to(dtype=dtype)
|
| 184 |
+
|
| 185 |
+
# ----- x_t (forward process) -----
|
| 186 |
+
noise = torch.randn_like(x0)
|
| 187 |
+
sigma_cur_ = sigma_cur.view(-1, *([1] * (x0.dim() - 1)))
|
| 188 |
+
x_t = sigma_cur_ * noise + (1.0 - sigma_cur_) * x0
|
| 189 |
+
|
| 190 |
+
# ----- student forward (grad) -----
|
| 191 |
+
v_student = AnimaBundle.dit_forward(student_transformer, x_t, sigma_cur, emb)
|
| 192 |
+
# phase-snap: student の "予測" を当該 phase 終端まで進める
|
| 193 |
+
pred_phase, sigma_phase = euler_multiphase(x_t, v_student, index, solver)
|
| 194 |
+
|
| 195 |
+
# ----- target side (no_grad) -----
|
| 196 |
+
with torch.no_grad():
|
| 197 |
+
# CFG-aug w sample
|
| 198 |
+
if args.w_fixed >= 0:
|
| 199 |
+
w = torch.full((B,), args.w_fixed, device=device, dtype=dtype)
|
| 200 |
+
else:
|
| 201 |
+
w = torch.empty(B, device=device, dtype=dtype).uniform_(args.w_min, args.w_max)
|
| 202 |
+
v_t_cond = AnimaBundle.dit_forward(teacher_transformer, x_t, sigma_cur, emb)
|
| 203 |
+
v_t_uncond = AnimaBundle.dit_forward(teacher_transformer, x_t, sigma_cur, cond_neg_b)
|
| 204 |
+
w_ = w.view(-1, *([1] * (x_t.dim() - 1)))
|
| 205 |
+
v_cfg = v_t_uncond + w_ * (v_t_cond - v_t_uncond)
|
| 206 |
+
# 1 teacher step forward (x_t → x_prev)
|
| 207 |
+
dt = (sigma_prev - sigma_cur).view(-1, *([1] * (x_t.dim() - 1)))
|
| 208 |
+
x_prev = x_t + dt * v_cfg
|
| 209 |
+
# student-as-EMA target prediction at x_prev
|
| 210 |
+
v_tgt = AnimaBundle.dit_forward(student_transformer, x_prev, sigma_prev, emb)
|
| 211 |
+
target_phase, _ = euler_multiphase(x_prev, v_tgt, (index + 1).clamp(max=solver.num_steps - 1), solver)
|
| 212 |
+
|
| 213 |
+
# ----- loss -----
|
| 214 |
+
diff = pred_phase - target_phase.detach()
|
| 215 |
+
loss = pseudo_huber(diff, c=args.huber_c).mean() / args.grad_accum
|
| 216 |
+
loss.backward()
|
| 217 |
+
metrics = {
|
| 218 |
+
"loss": float((loss * args.grad_accum).detach()),
|
| 219 |
+
"pred_abs": float(pred_phase.detach().abs().mean()),
|
| 220 |
+
"target_abs": float(target_phase.detach().abs().mean()),
|
| 221 |
+
"w_mean": float(w.mean().detach()),
|
| 222 |
+
"sigma_cur_mean": float(sigma_cur.mean().detach()),
|
| 223 |
+
}
|
| 224 |
+
torch.nn.utils.clip_grad_norm_(student_params, args.grad_clip)
|
| 225 |
+
opt.step()
|
| 226 |
+
|
| 227 |
+
if step % args.log_every == 0:
|
| 228 |
+
metrics["step"] = step
|
| 229 |
+
metrics["elapsed"] = time.time() - t0
|
| 230 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 231 |
+
msg = " ".join(f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}"
|
| 232 |
+
for k, v in metrics.items() if k != "step")
|
| 233 |
+
print(f"[step {step}/{args.total_steps}] {msg}", flush=True)
|
| 234 |
+
|
| 235 |
+
if step > 0 and step % args.sample_every == 0:
|
| 236 |
+
save_lora_state(student_transformer, out_dir, f"pcm_step{step:05d}")
|
| 237 |
+
print(f"[save] pcm_step{step:05d}.safetensors", flush=True)
|
| 238 |
+
try:
|
| 239 |
+
import modal
|
| 240 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 241 |
+
except Exception:
|
| 242 |
+
pass
|
| 243 |
+
|
| 244 |
+
print("[done] saving final")
|
| 245 |
+
save_lora_state(student_transformer, out_dir, "pcm_final")
|
| 246 |
+
log_f.close()
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
main()
|
scripts/distill/train_reflow.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima Reflow distillation (InstaFlow / rfpp 流派)
|
| 4 |
+
================================================
|
| 5 |
+
|
| 6 |
+
理論的根拠:
|
| 7 |
+
- Anima は既に rectified flow なので「Reflow」(1 round) を当てるのが自然
|
| 8 |
+
- rfpp (NeurIPS 2024 "Improving the Training of Rectified Flows") の知見:
|
| 9 |
+
* U-shape t sampling
|
| 10 |
+
* LPIPS-Huber (latent Huber + 間欠的に pixel LPIPS) で 1-round で十分
|
| 11 |
+
* adversarial 不要 (LADD と独立カテゴリ)
|
| 12 |
+
- 出力: 4-step (or 1-step) で動く student LoRA
|
| 13 |
+
|
| 14 |
+
データ:
|
| 15 |
+
事前に (noise, x0, emb) triplet を precompute (--save-noise) しておく前提。
|
| 16 |
+
/dataset/teacher_x0_cache_with_noise/{noise,x0,emb}/{idx}.pt
|
| 17 |
+
metadata.json に noise_path も書かれていること。
|
| 18 |
+
|
| 19 |
+
訓練 1 step:
|
| 20 |
+
1. (noise_i, x0_i, emb_i) を batch load
|
| 21 |
+
2. t ~ U-shape on (0, 1)
|
| 22 |
+
3. x_t = (1 - t) * x0 + t * noise
|
| 23 |
+
4. v_target = noise - x0
|
| 24 |
+
5. v_pred = student.dit_forward(x_t, t, emb)
|
| 25 |
+
6. L_huber = huber(v_pred, v_target, delta=0.03)
|
| 26 |
+
7. 任意 (lpips-every step): x0_pred = x_t - t*v_pred、VAE decode → LPIPS vs x0
|
| 27 |
+
8. backward → student LoRA 更新
|
| 28 |
+
"""
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
import argparse
|
| 31 |
+
import copy
|
| 32 |
+
import json
|
| 33 |
+
import os
|
| 34 |
+
import sys
|
| 35 |
+
import time
|
| 36 |
+
from pathlib import Path
|
| 37 |
+
|
| 38 |
+
import torch
|
| 39 |
+
import torch.nn.functional as F
|
| 40 |
+
from torch.utils.data import DataLoader, Dataset
|
| 41 |
+
from safetensors.torch import save_file, load_file
|
| 42 |
+
|
| 43 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 44 |
+
from distill.anima_loader import AnimaPaths, build_anima, AnimaBundle
|
| 45 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 46 |
+
from distill.train_traj import load_warm_lora, save_lora_state
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class ReflowPairDataset(Dataset):
|
| 50 |
+
"""precompute された (noise, x0, emb) triplet を読む。
|
| 51 |
+
metadata.json の各 entry に noise_path / x0_path / emb_path が必要。"""
|
| 52 |
+
def __init__(self, cache_dir: str | Path):
|
| 53 |
+
self.cache_dir = Path(cache_dir)
|
| 54 |
+
self.meta = json.loads((self.cache_dir / "metadata.json").read_text(encoding="utf-8"))
|
| 55 |
+
if not self.meta or "noise_path" not in self.meta[0]:
|
| 56 |
+
raise RuntimeError(
|
| 57 |
+
f"{self.cache_dir}/metadata.json has no 'noise_path' — "
|
| 58 |
+
"rerun precompute with --save-noise."
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
def __len__(self):
|
| 62 |
+
return len(self.meta)
|
| 63 |
+
|
| 64 |
+
def __getitem__(self, idx):
|
| 65 |
+
m = self.meta[idx]
|
| 66 |
+
x0 = torch.load(m["x0_path"], map_location="cpu", weights_only=True).squeeze(0)
|
| 67 |
+
noise = torch.load(m["noise_path"], map_location="cpu", weights_only=True).squeeze(0)
|
| 68 |
+
emb = torch.load(m["emb_path"], map_location="cpu", weights_only=True).squeeze(0)
|
| 69 |
+
return {"x0": x0, "noise": noise, "emb": emb}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def reflow_collate(batch):
|
| 73 |
+
return {
|
| 74 |
+
"x0": torch.stack([b["x0"] for b in batch]),
|
| 75 |
+
"noise": torch.stack([b["noise"] for b in batch]),
|
| 76 |
+
"emb": torch.stack([b["emb"] for b in batch]),
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def u_shape_t(B: int, device, dtype=torch.float32) -> torch.Tensor:
|
| 81 |
+
"""rfpp 流の U-shape sampler。両端 (t≈0, t≈1) に分布が偏る。
|
| 82 |
+
実装: u ~ U(0,1) → t = 0.5 * (1 + sign(u-0.5) * |2u-1|^0.5)
|
| 83 |
+
(sqrt によって両端密度上昇)"""
|
| 84 |
+
u = torch.rand(B, device=device, dtype=dtype)
|
| 85 |
+
centered = 2 * u - 1
|
| 86 |
+
t = 0.5 * (1.0 + torch.sign(centered) * centered.abs().sqrt())
|
| 87 |
+
return t.clamp(1e-3, 1.0 - 1e-3)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def main():
|
| 91 |
+
ap = argparse.ArgumentParser()
|
| 92 |
+
ap.add_argument("--cache-dir", required=True, type=str,
|
| 93 |
+
help="--save-noise 付きで precompute した cache")
|
| 94 |
+
ap.add_argument("--out", required=True, type=str)
|
| 95 |
+
ap.add_argument("--warm-lora", default="", type=str)
|
| 96 |
+
ap.add_argument("--total-steps", type=int, default=8000)
|
| 97 |
+
ap.add_argument("--batch-size", type=int, default=4)
|
| 98 |
+
ap.add_argument("--grad-accum", type=int, default=2)
|
| 99 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 100 |
+
ap.add_argument("--lr", type=float, default=1e-4)
|
| 101 |
+
ap.add_argument("--weight-decay", type=float, default=0.01)
|
| 102 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 103 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 104 |
+
ap.add_argument("--huber-delta", type=float, default=0.03)
|
| 105 |
+
ap.add_argument("--lpips-weight", type=float, default=0.1)
|
| 106 |
+
ap.add_argument("--lpips-every", type=int, default=4,
|
| 107 |
+
help="N step ごとに LPIPS reg を加算 (cost 削減)")
|
| 108 |
+
ap.add_argument("--t-sampler", default="u_shape", choices=["u_shape", "uniform"])
|
| 109 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 110 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 111 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 112 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 113 |
+
args = ap.parse_args()
|
| 114 |
+
|
| 115 |
+
torch.manual_seed(args.seed)
|
| 116 |
+
device = torch.device("cuda")
|
| 117 |
+
dtype = torch.bfloat16
|
| 118 |
+
out_dir = Path(args.out)
|
| 119 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 120 |
+
|
| 121 |
+
# ----- load Anima base (teacher は捨てる、cache が teacher 役) -----
|
| 122 |
+
print("[load] Anima bundle (student only — teacher cached on disk)")
|
| 123 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 124 |
+
|
| 125 |
+
# ----- student = wide LoRA -----
|
| 126 |
+
student_transformer = attach_wide_lora(bundle.transformer, rank=args.lora_rank)
|
| 127 |
+
student_transformer.to(device=device, dtype=dtype)
|
| 128 |
+
for n, p in student_transformer.named_parameters():
|
| 129 |
+
p.requires_grad = ("lora_" in n)
|
| 130 |
+
student_params = [p for p in student_transformer.parameters() if p.requires_grad]
|
| 131 |
+
print(f"[setup] student trainable: {sum(p.numel() for p in student_params)/1e6:.1f}M")
|
| 132 |
+
bundle.transformer = student_transformer
|
| 133 |
+
|
| 134 |
+
# warm-start
|
| 135 |
+
if args.warm_lora:
|
| 136 |
+
load_warm_lora(student_transformer, args.warm_lora)
|
| 137 |
+
|
| 138 |
+
# LPIPS (任意)
|
| 139 |
+
lpips_fn = None
|
| 140 |
+
if args.lpips_weight > 0:
|
| 141 |
+
import lpips as _lpips
|
| 142 |
+
lpips_fn = _lpips.LPIPS(net="alex").to(device).eval()
|
| 143 |
+
for p in lpips_fn.parameters():
|
| 144 |
+
p.requires_grad = False
|
| 145 |
+
print("[setup] LPIPS(alex) loaded")
|
| 146 |
+
|
| 147 |
+
# ----- optimizer -----
|
| 148 |
+
opt = torch.optim.AdamW(student_params, lr=args.lr, betas=(0.9, 0.999),
|
| 149 |
+
weight_decay=args.weight_decay, eps=1e-8)
|
| 150 |
+
|
| 151 |
+
# ----- dataset -----
|
| 152 |
+
print(f"[data] loading cache {args.cache_dir}")
|
| 153 |
+
dataset = ReflowPairDataset(args.cache_dir)
|
| 154 |
+
print(f" {len(dataset)} (noise, x0, emb) triplets")
|
| 155 |
+
loader = DataLoader(
|
| 156 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 157 |
+
num_workers=args.num_workers, collate_fn=reflow_collate,
|
| 158 |
+
drop_last=True, pin_memory=True,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
# ----- training loop -----
|
| 162 |
+
print(f"[train] steps={args.total_steps} bs={args.batch_size} accum={args.grad_accum} "
|
| 163 |
+
f"lpips_w={args.lpips_weight} every={args.lpips_every}")
|
| 164 |
+
log_path = out_dir / "reflow_log.jsonl"
|
| 165 |
+
log_f = open(log_path, "a", buffering=1)
|
| 166 |
+
t0 = time.time()
|
| 167 |
+
data_iter = iter(loader)
|
| 168 |
+
|
| 169 |
+
def _next():
|
| 170 |
+
nonlocal data_iter
|
| 171 |
+
try:
|
| 172 |
+
return next(data_iter)
|
| 173 |
+
except StopIteration:
|
| 174 |
+
data_iter = iter(loader)
|
| 175 |
+
return next(data_iter)
|
| 176 |
+
|
| 177 |
+
for step in range(args.total_steps):
|
| 178 |
+
student_transformer.train()
|
| 179 |
+
opt.zero_grad()
|
| 180 |
+
metrics = {}
|
| 181 |
+
for _ in range(args.grad_accum):
|
| 182 |
+
batch = _next()
|
| 183 |
+
x0 = batch["x0"].to(device=device, dtype=dtype)
|
| 184 |
+
noise = batch["noise"].to(device=device, dtype=dtype)
|
| 185 |
+
emb = batch["emb"].to(device=device, dtype=dtype)
|
| 186 |
+
B = x0.size(0)
|
| 187 |
+
|
| 188 |
+
# ----- t sampling -----
|
| 189 |
+
if args.t_sampler == "u_shape":
|
| 190 |
+
t = u_shape_t(B, device, dtype=torch.float32).to(dtype=dtype)
|
| 191 |
+
else:
|
| 192 |
+
t = torch.rand(B, device=device, dtype=dtype).clamp(1e-3, 1.0 - 1e-3)
|
| 193 |
+
|
| 194 |
+
t_ = t.view(-1, *([1] * (x0.dim() - 1)))
|
| 195 |
+
x_t = (1 - t_) * x0 + t_ * noise
|
| 196 |
+
v_target = noise - x0
|
| 197 |
+
|
| 198 |
+
# ----- student forward (1 forward only) -----
|
| 199 |
+
v_pred = AnimaBundle.dit_forward(student_transformer, x_t, t, emb)
|
| 200 |
+
l_huber = F.huber_loss(v_pred.float(), v_target.float(),
|
| 201 |
+
delta=args.huber_delta, reduction="mean")
|
| 202 |
+
|
| 203 |
+
# ----- LPIPS reg (every N steps) -----
|
| 204 |
+
l_lpips = torch.zeros((), device=device)
|
| 205 |
+
if lpips_fn is not None and (step % args.lpips_every) == 0:
|
| 206 |
+
x0_pred = x_t - t_ * v_pred
|
| 207 |
+
vae_dtype = next(bundle.vae.model.parameters()).dtype
|
| 208 |
+
img_p = bundle.vae.model.decode(x0_pred.to(dtype=vae_dtype), bundle.vae_scale).squeeze(2)
|
| 209 |
+
with torch.no_grad():
|
| 210 |
+
img_t = bundle.vae.model.decode(x0.to(dtype=vae_dtype), bundle.vae_scale).squeeze(2)
|
| 211 |
+
l_lpips = lpips_fn(img_p.float(), img_t.float()).mean()
|
| 212 |
+
|
| 213 |
+
loss = (l_huber + args.lpips_weight * l_lpips) / args.grad_accum
|
| 214 |
+
loss.backward()
|
| 215 |
+
metrics = {
|
| 216 |
+
"l_huber": float(l_huber.detach()),
|
| 217 |
+
"l_lpips": float(l_lpips.detach()),
|
| 218 |
+
"loss": float((l_huber + args.lpips_weight * l_lpips).detach()),
|
| 219 |
+
}
|
| 220 |
+
torch.nn.utils.clip_grad_norm_(student_params, args.grad_clip)
|
| 221 |
+
opt.step()
|
| 222 |
+
|
| 223 |
+
if step % args.log_every == 0:
|
| 224 |
+
metrics["step"] = step
|
| 225 |
+
metrics["elapsed"] = time.time() - t0
|
| 226 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 227 |
+
msg = " ".join(f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}"
|
| 228 |
+
for k, v in metrics.items() if k != "step")
|
| 229 |
+
print(f"[step {step}/{args.total_steps}] {msg}", flush=True)
|
| 230 |
+
|
| 231 |
+
if step > 0 and step % args.sample_every == 0:
|
| 232 |
+
save_lora_state(student_transformer, out_dir, f"reflow_step{step:05d}")
|
| 233 |
+
print(f"[save] reflow_step{step:05d}.safetensors", flush=True)
|
| 234 |
+
try:
|
| 235 |
+
import modal
|
| 236 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 237 |
+
except Exception:
|
| 238 |
+
pass
|
| 239 |
+
|
| 240 |
+
print("[done] saving final")
|
| 241 |
+
save_lora_state(student_transformer, out_dir, "reflow_final")
|
| 242 |
+
log_f.close()
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
if __name__ == "__main__":
|
| 246 |
+
main()
|
scripts/distill/train_shortcut.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima Shortcut Models distillation (Frans et al., 2024)
|
| 4 |
+
|
| 5 |
+
特徴:
|
| 6 |
+
- 単一 LoRA、d を入力で取って 1/2/4/8/128-step 自在に切替可能
|
| 7 |
+
- Flow-matching half (d=0) + Bootstrap half (d>0) を 1 step 内で混在
|
| 8 |
+
- PCM と違い phase 固定なし、d を連続値で扱える
|
| 9 |
+
|
| 10 |
+
データ:
|
| 11 |
+
Reflow cache (--save-noise 付き) を流用。(noise, x0, emb) triplet。
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
import argparse
|
| 15 |
+
import copy
|
| 16 |
+
import json
|
| 17 |
+
import math
|
| 18 |
+
import os
|
| 19 |
+
import sys
|
| 20 |
+
import time
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn.functional as F
|
| 25 |
+
from torch.utils.data import DataLoader
|
| 26 |
+
|
| 27 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 28 |
+
from distill.anima_loader import build_anima, AnimaBundle
|
| 29 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 30 |
+
from distill.train_traj import load_warm_lora, save_lora_state
|
| 31 |
+
from distill.train_reflow import ReflowPairDataset, reflow_collate
|
| 32 |
+
from distill.shortcut_module import attach_shortcut_d_head, set_shortcut_d, shortcut_d_head_params
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def main():
|
| 36 |
+
ap = argparse.ArgumentParser()
|
| 37 |
+
ap.add_argument("--cache-dir", required=True, type=str,
|
| 38 |
+
help="Reflow cache (--save-noise 付き)")
|
| 39 |
+
ap.add_argument("--out", required=True, type=str)
|
| 40 |
+
ap.add_argument("--warm-lora", default="", type=str)
|
| 41 |
+
ap.add_argument("--total-steps", type=int, default=2000)
|
| 42 |
+
ap.add_argument("--batch-size", type=int, default=4)
|
| 43 |
+
ap.add_argument("--grad-accum", type=int, default=2)
|
| 44 |
+
ap.add_argument("--denoise-timesteps", type=int, default=128,
|
| 45 |
+
help="discrete grid for sampling t and d (2^k granularity)")
|
| 46 |
+
ap.add_argument("--bootstrap-every", type=int, default=8,
|
| 47 |
+
help="batch 内 bootstrap 比率: 1/N (paper default 8)")
|
| 48 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 49 |
+
ap.add_argument("--lr", type=float, default=2e-5)
|
| 50 |
+
ap.add_argument("--lr-d-head", type=float, default=5e-4,
|
| 51 |
+
help="d_head は zero-init なので高めの lr で立ち上げ")
|
| 52 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 53 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 54 |
+
ap.add_argument("--clip-x-bootstrap", type=float, default=4.0,
|
| 55 |
+
help="bootstrap 中の x_t clip range")
|
| 56 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 57 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 58 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 59 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 60 |
+
args = ap.parse_args()
|
| 61 |
+
|
| 62 |
+
torch.manual_seed(args.seed)
|
| 63 |
+
device = torch.device("cuda")
|
| 64 |
+
dtype = torch.bfloat16
|
| 65 |
+
out_dir = Path(args.out)
|
| 66 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
|
| 68 |
+
print("[load] Anima bundle")
|
| 69 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 70 |
+
|
| 71 |
+
# student = wide LoRA + d-head
|
| 72 |
+
student_transformer = attach_wide_lora(bundle.transformer, rank=args.lora_rank)
|
| 73 |
+
student_transformer.to(device=device, dtype=dtype)
|
| 74 |
+
for n, p in student_transformer.named_parameters():
|
| 75 |
+
p.requires_grad = ("lora_" in n)
|
| 76 |
+
attach_shortcut_d_head(student_transformer) # adds d_head + hook + _current_d attr
|
| 77 |
+
# d_head の params は trainable に
|
| 78 |
+
d_head_params = shortcut_d_head_params(student_transformer)
|
| 79 |
+
for p in d_head_params:
|
| 80 |
+
p.requires_grad = True
|
| 81 |
+
student_lora_params = [p for n, p in student_transformer.named_parameters()
|
| 82 |
+
if p.requires_grad and "lora_" in n]
|
| 83 |
+
print(f"[setup] student LoRA: {sum(p.numel() for p in student_lora_params)/1e6:.1f}M")
|
| 84 |
+
print(f"[setup] d_head: {sum(p.numel() for p in d_head_params)/1e6:.1f}M")
|
| 85 |
+
bundle.transformer = student_transformer
|
| 86 |
+
|
| 87 |
+
if args.warm_lora:
|
| 88 |
+
load_warm_lora(student_transformer, args.warm_lora)
|
| 89 |
+
|
| 90 |
+
# optimizers (LoRA と d_head で別 lr)
|
| 91 |
+
opt_lora = torch.optim.AdamW(student_lora_params, lr=args.lr,
|
| 92 |
+
betas=(0.9, 0.999), weight_decay=0.01)
|
| 93 |
+
opt_d_head = torch.optim.AdamW(d_head_params, lr=args.lr_d_head,
|
| 94 |
+
betas=(0.9, 0.999), weight_decay=0.0)
|
| 95 |
+
|
| 96 |
+
# dataset
|
| 97 |
+
print(f"[data] {args.cache_dir}")
|
| 98 |
+
dataset = ReflowPairDataset(args.cache_dir)
|
| 99 |
+
print(f" {len(dataset)} triplets")
|
| 100 |
+
loader = DataLoader(
|
| 101 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 102 |
+
num_workers=args.num_workers, collate_fn=reflow_collate,
|
| 103 |
+
drop_last=True, pin_memory=True,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
T = args.denoise_timesteps
|
| 107 |
+
log2_sections = int(math.log2(T)) # 7 for T=128
|
| 108 |
+
|
| 109 |
+
def student_v_with_d(x, t, cond, d):
|
| 110 |
+
set_shortcut_d(student_transformer, d)
|
| 111 |
+
try:
|
| 112 |
+
return AnimaBundle.dit_forward(student_transformer, x, t, cond)
|
| 113 |
+
finally:
|
| 114 |
+
set_shortcut_d(student_transformer, None)
|
| 115 |
+
|
| 116 |
+
print(f"[train] steps={args.total_steps} bs={args.batch_size} accum={args.grad_accum} "
|
| 117 |
+
f"T={T} bootstrap_every={args.bootstrap_every}")
|
| 118 |
+
log_path = out_dir / "shortcut_log.jsonl"
|
| 119 |
+
log_f = open(log_path, "a", buffering=1)
|
| 120 |
+
t0 = time.time()
|
| 121 |
+
data_iter = iter(loader)
|
| 122 |
+
|
| 123 |
+
def _next():
|
| 124 |
+
nonlocal data_iter
|
| 125 |
+
try:
|
| 126 |
+
return next(data_iter)
|
| 127 |
+
except StopIteration:
|
| 128 |
+
data_iter = iter(loader)
|
| 129 |
+
return next(data_iter)
|
| 130 |
+
|
| 131 |
+
for step in range(args.total_steps):
|
| 132 |
+
student_transformer.train()
|
| 133 |
+
opt_lora.zero_grad()
|
| 134 |
+
opt_d_head.zero_grad()
|
| 135 |
+
metrics = {}
|
| 136 |
+
for _ in range(args.grad_accum):
|
| 137 |
+
batch = _next()
|
| 138 |
+
x0 = batch["x0"].to(device=device, dtype=dtype)
|
| 139 |
+
noise = batch["noise"].to(device=device, dtype=dtype)
|
| 140 |
+
emb = batch["emb"].to(device=device, dtype=dtype)
|
| 141 |
+
B = x0.size(0)
|
| 142 |
+
B_boot = max(1, B // args.bootstrap_every)
|
| 143 |
+
B_flow = B - B_boot
|
| 144 |
+
|
| 145 |
+
# ----- Flow-matching half (d=0, infinitesimal) -----
|
| 146 |
+
t_fm_idx = torch.randint(0, T, (B_flow,), device=device)
|
| 147 |
+
t_fm = t_fm_idx.float() / T
|
| 148 |
+
t_fm_b = t_fm.view(-1, *([1] * (x0.dim() - 1)))
|
| 149 |
+
x0_fm = x0[:B_flow]; noise_fm = noise[:B_flow]; emb_fm = emb[:B_flow]
|
| 150 |
+
x_t_fm = (1 - t_fm_b) * x0_fm + t_fm_b * noise_fm
|
| 151 |
+
v_tgt_fm = noise_fm - x0_fm
|
| 152 |
+
d_fm = torch.zeros(B_flow, device=device, dtype=dtype)
|
| 153 |
+
|
| 154 |
+
# ----- Bootstrap half (d > 0, self-consistency) -----
|
| 155 |
+
if B_boot > 0:
|
| 156 |
+
k = torch.randint(0, log2_sections, (B_boot,), device=device)
|
| 157 |
+
d_b = (1.0 / (2.0 ** k.float())).to(dtype=dtype) # d ∈ {1/2^k}
|
| 158 |
+
# t aligned to d grid: t_b = (random int) / 2^k * 1
|
| 159 |
+
t_b_max = (2 ** k).float()
|
| 160 |
+
t_b_idx = (torch.rand(B_boot, device=device) * t_b_max).floor()
|
| 161 |
+
t_b = t_b_idx / t_b_max.clamp(min=1.0)
|
| 162 |
+
t_b_b = t_b.view(-1, *([1] * (x0.dim() - 1)))
|
| 163 |
+
x0_b = x0[B_flow:]; noise_b = noise[B_flow:]; emb_b = emb[B_flow:]
|
| 164 |
+
x_t_b = (1 - t_b_b) * x0_b + t_b_b * noise_b
|
| 165 |
+
|
| 166 |
+
# 2 sub-step bootstrap target (no_grad)
|
| 167 |
+
with torch.no_grad():
|
| 168 |
+
d_half = d_b * 0.5
|
| 169 |
+
v1 = student_v_with_d(x_t_b, t_b.to(dtype=dtype), emb_b, d_half)
|
| 170 |
+
dt_half = d_half.view(-1, *([1] * (x_t_b.dim() - 1)))
|
| 171 |
+
x_t2 = (x_t_b + dt_half * v1).clamp(-args.clip_x_bootstrap, args.clip_x_bootstrap)
|
| 172 |
+
t_b_half = (t_b + d_half.float()).clamp(0.0, 1.0)
|
| 173 |
+
v2 = student_v_with_d(x_t2, t_b_half.to(dtype=dtype), emb_b, d_half)
|
| 174 |
+
v_tgt_b = 0.5 * (v1 + v2)
|
| 175 |
+
|
| 176 |
+
# concat batch
|
| 177 |
+
x_cat = torch.cat([x_t_fm, x_t_b], dim=0)
|
| 178 |
+
t_cat = torch.cat([t_fm.to(dtype=dtype), t_b.to(dtype=dtype)], dim=0)
|
| 179 |
+
d_cat = torch.cat([d_fm, d_b], dim=0)
|
| 180 |
+
emb_cat = torch.cat([emb_fm, emb_b], dim=0)
|
| 181 |
+
v_tgt_cat = torch.cat([v_tgt_fm, v_tgt_b], dim=0)
|
| 182 |
+
else:
|
| 183 |
+
x_cat, t_cat, d_cat, emb_cat, v_tgt_cat = x_t_fm, t_fm.to(dtype=dtype), d_fm, emb_fm, v_tgt_fm
|
| 184 |
+
|
| 185 |
+
# 1 trainable forward
|
| 186 |
+
v_pred = student_v_with_d(x_cat, t_cat, emb_cat, d_cat)
|
| 187 |
+
loss = F.mse_loss(v_pred.float(), v_tgt_cat.detach().float()) / args.grad_accum
|
| 188 |
+
loss.backward()
|
| 189 |
+
metrics = {
|
| 190 |
+
"loss": float((loss * args.grad_accum).detach()),
|
| 191 |
+
"B_flow": B_flow, "B_boot": B_boot,
|
| 192 |
+
"v_pred_abs": float(v_pred.detach().abs().mean()),
|
| 193 |
+
"v_tgt_abs": float(v_tgt_cat.detach().abs().mean()),
|
| 194 |
+
}
|
| 195 |
+
torch.nn.utils.clip_grad_norm_(student_lora_params + d_head_params, args.grad_clip)
|
| 196 |
+
opt_lora.step()
|
| 197 |
+
opt_d_head.step()
|
| 198 |
+
|
| 199 |
+
if step % args.log_every == 0:
|
| 200 |
+
metrics["step"] = step
|
| 201 |
+
metrics["elapsed"] = time.time() - t0
|
| 202 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 203 |
+
msg = " ".join(f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}"
|
| 204 |
+
for k, v in metrics.items() if k != "step")
|
| 205 |
+
print(f"[step {step}/{args.total_steps}] {msg}", flush=True)
|
| 206 |
+
|
| 207 |
+
if step > 0 and step % args.sample_every == 0:
|
| 208 |
+
# LoRA + d_head 両方保存
|
| 209 |
+
save_lora_state(student_transformer, out_dir, f"shortcut_step{step:05d}")
|
| 210 |
+
torch.save(student_transformer.d_head.state_dict(),
|
| 211 |
+
out_dir / f"shortcut_d_head_step{step:05d}.pt")
|
| 212 |
+
print(f"[save] shortcut_step{step:05d}", flush=True)
|
| 213 |
+
try:
|
| 214 |
+
import modal
|
| 215 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 216 |
+
except Exception:
|
| 217 |
+
pass
|
| 218 |
+
|
| 219 |
+
print("[done] saving final")
|
| 220 |
+
save_lora_state(student_transformer, out_dir, "shortcut_final")
|
| 221 |
+
torch.save(student_transformer.d_head.state_dict(), out_dir / "shortcut_d_head_final.pt")
|
| 222 |
+
log_f.close()
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
if __name__ == "__main__":
|
| 226 |
+
main()
|
scripts/distill/train_sid.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima SiD2 / SiD-DiT distillation
|
| 4 |
+
=================================
|
| 5 |
+
|
| 6 |
+
DMD2 と同じ「2 LoRA adapter on shared base」パターンで実装。違いは:
|
| 7 |
+
- D も EMA も不要 (D は SiD identity formula に置換)
|
| 8 |
+
- data-free: caption のみで OK (画像は使わない)
|
| 9 |
+
- ψ (fake_score) を 200 step warmup してから generator を活性化
|
| 10 |
+
- ratio: ψ:θ = 1:1 (DMD2 の 5:1 と異なる)
|
| 11 |
+
|
| 12 |
+
使い方:
|
| 13 |
+
modal run modal_app.py::train_sid_distill \\
|
| 14 |
+
--total-outer-steps 8000 --warm-lora /models/loras/anima_turbo.safetensors
|
| 15 |
+
|
| 16 |
+
注意:
|
| 17 |
+
cache-dir は emb のみ使うので LADD cache でも reflow cache でも何でも良い。
|
| 18 |
+
画像が無い caption だけのテキストファイル群でも、本質的には動く
|
| 19 |
+
(今回は既存 cache の emb を流用してコスト 0)。
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
import argparse
|
| 23 |
+
import copy
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import sys
|
| 27 |
+
import time
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
|
| 30 |
+
import torch
|
| 31 |
+
from torch.utils.data import DataLoader
|
| 32 |
+
|
| 33 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 34 |
+
from distill.anima_loader import build_anima, AnimaBundle
|
| 35 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 36 |
+
from distill.train_dmd2_official import (
|
| 37 |
+
attach_dual_lora, make_velocity_fn, make_teacher_velocity_fn,
|
| 38 |
+
)
|
| 39 |
+
from distill.train_traj import save_lora_state, convert_comfy_to_peft_lora
|
| 40 |
+
from distill.sid_loss import sid_generator_loss, sid_score_helper_loss
|
| 41 |
+
from distill.train_ladd import PrecomputedCacheDataset, ladd_collate
|
| 42 |
+
from safetensors.torch import load_file
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main():
|
| 46 |
+
ap = argparse.ArgumentParser()
|
| 47 |
+
ap.add_argument("--cache-dir", required=True, type=str,
|
| 48 |
+
help="emb のみ使用 (画像は不要)。LADD/Reflow cache 流用 OK")
|
| 49 |
+
ap.add_argument("--out", required=True, type=str)
|
| 50 |
+
ap.add_argument("--warm-lora", default="", type=str)
|
| 51 |
+
ap.add_argument("--total-outer-steps", type=int, default=8000)
|
| 52 |
+
ap.add_argument("--psi-warmup-steps", type=int, default=200,
|
| 53 |
+
help="ψ を先に warmup し、それ以降 θ を活性化")
|
| 54 |
+
ap.add_argument("--n-student-steps", type=int, default=4)
|
| 55 |
+
ap.add_argument("--batch-size", type=int, default=2)
|
| 56 |
+
ap.add_argument("--grad-accum", type=int, default=2)
|
| 57 |
+
ap.add_argument("--resolution", type=int, default=768)
|
| 58 |
+
ap.add_argument("--teacher-cfg", type=float, default=4.5)
|
| 59 |
+
ap.add_argument("--student-cfg", type=float, default=1.0)
|
| 60 |
+
ap.add_argument("--alpha", type=float, default=1.2, help="SiD2 identity weight")
|
| 61 |
+
ap.add_argument("--mu-t", type=float, default=0.6931, help="ln 2")
|
| 62 |
+
ap.add_argument("--sigma-t", type=float, default=1.6)
|
| 63 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 64 |
+
ap.add_argument("--lr-gen", type=float, default=1e-5)
|
| 65 |
+
ap.add_argument("--lr-psi", type=float, default=2e-5)
|
| 66 |
+
ap.add_argument("--weight-decay", type=float, default=0.01)
|
| 67 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 68 |
+
ap.add_argument("--neg-prompt", default="")
|
| 69 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 70 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 71 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 72 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 73 |
+
args = ap.parse_args()
|
| 74 |
+
|
| 75 |
+
torch.manual_seed(args.seed)
|
| 76 |
+
device = torch.device("cuda")
|
| 77 |
+
dtype = torch.bfloat16
|
| 78 |
+
out_dir = Path(args.out)
|
| 79 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 80 |
+
|
| 81 |
+
print("[load] Anima bundle")
|
| 82 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 83 |
+
|
| 84 |
+
# teacher = deepcopy
|
| 85 |
+
print("[setup] teacher = frozen deepcopy")
|
| 86 |
+
teacher_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype).eval()
|
| 87 |
+
for p in teacher_transformer.parameters():
|
| 88 |
+
p.requires_grad = False
|
| 89 |
+
|
| 90 |
+
# dual-adapter (student + fake_score / SiD-DiT は ψ と呼ぶ)
|
| 91 |
+
print("[setup] dual-adapter (student + psi)")
|
| 92 |
+
peft_model = attach_dual_lora(bundle.transformer, rank=args.lora_rank,
|
| 93 |
+
adapter_names=("student", "psi"))
|
| 94 |
+
peft_model.to(device=device, dtype=dtype)
|
| 95 |
+
for n, p in peft_model.named_parameters():
|
| 96 |
+
p.requires_grad = ("lora_" in n)
|
| 97 |
+
bundle.transformer = peft_model
|
| 98 |
+
|
| 99 |
+
student_params = [p for n, p in peft_model.named_parameters()
|
| 100 |
+
if p.requires_grad and ".student." in n]
|
| 101 |
+
psi_params = [p for n, p in peft_model.named_parameters()
|
| 102 |
+
if p.requires_grad and ".psi." in n]
|
| 103 |
+
print(f"[setup] student: {sum(p.numel() for p in student_params)/1e6:.1f}M")
|
| 104 |
+
print(f"[setup] psi: {sum(p.numel() for p in psi_params)/1e6:.1f}M")
|
| 105 |
+
|
| 106 |
+
# warm-start (student に注入)
|
| 107 |
+
if args.warm_lora:
|
| 108 |
+
print(f"[warm] loading {args.warm_lora} → student adapter")
|
| 109 |
+
sd_comfy = load_file(args.warm_lora)
|
| 110 |
+
sd_peft_default = convert_comfy_to_peft_lora(sd_comfy)
|
| 111 |
+
sd_student = {}
|
| 112 |
+
for k, v in sd_peft_default.items():
|
| 113 |
+
nk = k.replace(".lora_A.default.weight", ".lora_A.student.weight")
|
| 114 |
+
nk = nk.replace(".lora_B.default.weight", ".lora_B.student.weight")
|
| 115 |
+
sd_student[nk] = v
|
| 116 |
+
model_sd = peft_model.state_dict()
|
| 117 |
+
to_load = {k: v.to(dtype=model_sd[k].dtype) for k, v in sd_student.items()
|
| 118 |
+
if k in model_sd and model_sd[k].shape == v.shape}
|
| 119 |
+
print(f"[warm] matched {len(to_load)}/{len(sd_student)} keys")
|
| 120 |
+
peft_model.load_state_dict(to_load, strict=False)
|
| 121 |
+
|
| 122 |
+
# optimizers
|
| 123 |
+
opt_gen = torch.optim.AdamW(student_params, lr=args.lr_gen,
|
| 124 |
+
betas=(0.9, 0.999), weight_decay=args.weight_decay)
|
| 125 |
+
opt_psi = torch.optim.AdamW(psi_params, lr=args.lr_psi,
|
| 126 |
+
betas=(0.9, 0.999), weight_decay=args.weight_decay)
|
| 127 |
+
|
| 128 |
+
# velocity functions
|
| 129 |
+
student_v = make_velocity_fn(peft_model, "student")
|
| 130 |
+
psi_v = make_velocity_fn(peft_model, "psi")
|
| 131 |
+
teacher_v = make_teacher_velocity_fn(teacher_transformer)
|
| 132 |
+
|
| 133 |
+
# dataset (emb のみ使う、x0 は initial noise の source として扱う)
|
| 134 |
+
print(f"[data] {args.cache_dir}")
|
| 135 |
+
dataset = PrecomputedCacheDataset(args.cache_dir)
|
| 136 |
+
print(f" {len(dataset)} captions")
|
| 137 |
+
loader = DataLoader(
|
| 138 |
+
dataset, batch_size=args.batch_size, shuffle=True,
|
| 139 |
+
num_workers=args.num_workers, collate_fn=ladd_collate,
|
| 140 |
+
drop_last=True, pin_memory=True,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
with torch.no_grad():
|
| 144 |
+
cond_neg = bundle.text_encode([args.neg_prompt or ""])
|
| 145 |
+
|
| 146 |
+
H_lat = args.resolution // 8
|
| 147 |
+
W_lat = args.resolution // 8
|
| 148 |
+
|
| 149 |
+
print(f"[train] outer={args.total_outer_steps} bs={args.batch_size} accum={args.grad_accum} "
|
| 150 |
+
f"psi_warmup={args.psi_warmup_steps} n_steps={args.n_student_steps}")
|
| 151 |
+
log_path = out_dir / "sid_log.jsonl"
|
| 152 |
+
log_f = open(log_path, "a", buffering=1)
|
| 153 |
+
t0 = time.time()
|
| 154 |
+
data_iter = iter(loader)
|
| 155 |
+
|
| 156 |
+
def _next():
|
| 157 |
+
nonlocal data_iter
|
| 158 |
+
try:
|
| 159 |
+
return next(data_iter)
|
| 160 |
+
except StopIteration:
|
| 161 |
+
data_iter = iter(loader)
|
| 162 |
+
return next(data_iter)
|
| 163 |
+
|
| 164 |
+
def _sample_inputs():
|
| 165 |
+
batch = _next()
|
| 166 |
+
cond_pos = batch["emb"].to(device=device, dtype=dtype)
|
| 167 |
+
B = cond_pos.size(0)
|
| 168 |
+
cond_neg_b = cond_neg.expand(B, -1, -1).contiguous() if B > 1 else cond_neg
|
| 169 |
+
noise = torch.randn(B, 16, 1, H_lat, W_lat, device=device, dtype=dtype)
|
| 170 |
+
return noise, cond_pos, cond_neg_b
|
| 171 |
+
|
| 172 |
+
for outer in range(args.total_outer_steps):
|
| 173 |
+
# ---- ψ (score helper) update (every step, including warmup) ----
|
| 174 |
+
peft_model.train()
|
| 175 |
+
opt_psi.zero_grad()
|
| 176 |
+
psi_metrics = {}
|
| 177 |
+
for _ in range(args.grad_accum):
|
| 178 |
+
noise, cond_pos, cond_neg_b = _sample_inputs()
|
| 179 |
+
L_psi, m_psi = sid_score_helper_loss(
|
| 180 |
+
student_v, psi_v, noise, cond_pos, cond_neg_b,
|
| 181 |
+
student_cfg=args.student_cfg, n_steps=args.n_student_steps,
|
| 182 |
+
mu_t=args.mu_t, sigma_t=args.sigma_t,
|
| 183 |
+
)
|
| 184 |
+
(L_psi / args.grad_accum).backward()
|
| 185 |
+
psi_metrics = {k: float(v) for k, v in m_psi.items()}
|
| 186 |
+
torch.nn.utils.clip_grad_norm_(psi_params, args.grad_clip)
|
| 187 |
+
opt_psi.step()
|
| 188 |
+
|
| 189 |
+
# ---- θ (generator) update (warmup 後のみ) ----
|
| 190 |
+
gen_metrics = {}
|
| 191 |
+
if outer >= args.psi_warmup_steps:
|
| 192 |
+
opt_gen.zero_grad()
|
| 193 |
+
for _ in range(args.grad_accum):
|
| 194 |
+
noise, cond_pos, cond_neg_b = _sample_inputs()
|
| 195 |
+
L_theta, m_theta = sid_generator_loss(
|
| 196 |
+
student_v, teacher_v, psi_v, noise, cond_pos, cond_neg_b,
|
| 197 |
+
teacher_cfg=args.teacher_cfg, student_cfg=args.student_cfg,
|
| 198 |
+
n_steps=args.n_student_steps, alpha=args.alpha,
|
| 199 |
+
mu_t=args.mu_t, sigma_t=args.sigma_t,
|
| 200 |
+
)
|
| 201 |
+
(L_theta / args.grad_accum).backward()
|
| 202 |
+
gen_metrics = {k: float(v) for k, v in m_theta.items()}
|
| 203 |
+
torch.nn.utils.clip_grad_norm_(student_params, args.grad_clip)
|
| 204 |
+
opt_gen.step()
|
| 205 |
+
|
| 206 |
+
if outer % args.log_every == 0:
|
| 207 |
+
metrics = {"outer": outer, "elapsed": time.time() - t0,
|
| 208 |
+
"phase": "warmup" if outer < args.psi_warmup_steps else "joint",
|
| 209 |
+
**psi_metrics, **gen_metrics}
|
| 210 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 211 |
+
msg = " ".join(f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}"
|
| 212 |
+
for k, v in metrics.items() if k not in ("outer",))
|
| 213 |
+
print(f"[outer {outer}/{args.total_outer_steps}] {msg}", flush=True)
|
| 214 |
+
|
| 215 |
+
if outer > 0 and outer % args.sample_every == 0:
|
| 216 |
+
sd = {k: v.detach().cpu() for k, v in peft_model.state_dict().items()
|
| 217 |
+
if "lora_" in k and ".student." in k}
|
| 218 |
+
from safetensors.torch import save_file as _sf
|
| 219 |
+
_sf(sd, str(out_dir / f"sid_student_step{outer:05d}.safetensors"))
|
| 220 |
+
print(f"[save] sid_student_step{outer:05d}.safetensors", flush=True)
|
| 221 |
+
try:
|
| 222 |
+
import modal
|
| 223 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 224 |
+
except Exception:
|
| 225 |
+
pass
|
| 226 |
+
|
| 227 |
+
print("[done] saving final")
|
| 228 |
+
sd_student_final = {k: v.detach().cpu() for k, v in peft_model.state_dict().items()
|
| 229 |
+
if "lora_" in k and ".student." in k}
|
| 230 |
+
from safetensors.torch import save_file as _sf
|
| 231 |
+
_sf(sd_student_final, str(out_dir / "sid_student_final.safetensors"))
|
| 232 |
+
log_f.close()
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
main()
|
scripts/distill/train_sota.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima SOTA distillation main entrypoint
|
| 4 |
+
========================================
|
| 5 |
+
|
| 6 |
+
3 phase progressive 蒸留:
|
| 7 |
+
Phase A: 8-step, DMD2 + light TSCD + light adv
|
| 8 |
+
Phase B: 4-step, balanced
|
| 9 |
+
Phase C: 2-step, full adv weight
|
| 10 |
+
|
| 11 |
+
各 phase 完了時に LoRA を保存し、次 phase へ。
|
| 12 |
+
|
| 13 |
+
使い方 (Modal 経由):
|
| 14 |
+
modal run --detach modal_app.py::train_sota_distill --phase a
|
| 15 |
+
modal run --detach modal_app.py::train_sota_distill --phase b --resume /output/distill/phase_a_final
|
| 16 |
+
modal run --detach modal_app.py::train_sota_distill --phase c --resume /output/distill/phase_b_final
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
import argparse
|
| 20 |
+
import copy
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import sys
|
| 24 |
+
import time
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
from torch.utils.data import DataLoader
|
| 29 |
+
from safetensors.torch import save_file
|
| 30 |
+
|
| 31 |
+
# import-path: 同ディレクトリ前提
|
| 32 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 33 |
+
from distill.anima_loader import AnimaPaths, build_anima
|
| 34 |
+
from distill.dataset import AnimaImageCaptionDataset, collate_fn
|
| 35 |
+
from distill.dmd2_trainer import (
|
| 36 |
+
DMD2Args, DMD2Trainer, attach_qv_lora, attach_wide_lora, set_lora_scale,
|
| 37 |
+
)
|
| 38 |
+
from distill.r3gan_disc import R3GANDiscriminator
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ----- phase 設定 -----------------------------------------------------------
|
| 42 |
+
# Phase A postmortem の教訓: R3GAN gamma calibration ミスで gen 破壊。
|
| 43 |
+
# Phase B/C は **R3GAN を完全 disable** (adv_weight=0) で DMD+TSCD のみで蒸留。
|
| 44 |
+
# R3GAN は後付け実験(別途 phase b_r3gan 等)で導入する想定。
|
| 45 |
+
PHASE_CONFIGS = {
|
| 46 |
+
"a": DMD2Args(
|
| 47 |
+
num_inference_steps=8,
|
| 48 |
+
tscd_weight=0.3, adv_weight=0.0, r3gan_gamma=0.0, # disabled
|
| 49 |
+
cfg_aug_cold_steps=200, dynamic_decay_steps=3000,
|
| 50 |
+
),
|
| 51 |
+
"b": DMD2Args(
|
| 52 |
+
num_inference_steps=4,
|
| 53 |
+
tscd_weight=0.5, adv_weight=0.0, r3gan_gamma=0.0, # disabled
|
| 54 |
+
cfg_aug_cold_steps=100, dynamic_decay_steps=2000,
|
| 55 |
+
),
|
| 56 |
+
"c": DMD2Args(
|
| 57 |
+
num_inference_steps=2,
|
| 58 |
+
tscd_weight=0.5, adv_weight=0.0, r3gan_gamma=0.0, # disabled
|
| 59 |
+
cfg_aug_cold_steps=50, dynamic_decay_steps=1500,
|
| 60 |
+
),
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def save_lora_state(model, path: Path, name: str) -> None:
|
| 65 |
+
"""PEFT-wrapped model から LoRA weight だけ抽出して safetensors で保存。"""
|
| 66 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
sd = {k: v.detach().cpu() for k, v in model.state_dict().items() if "lora_" in k}
|
| 68 |
+
save_file(sd, str(path / f"{name}.safetensors"))
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def save_full_state(state_dict: dict, path: Path) -> None:
|
| 72 |
+
"""フル状態(gen の全 weights + buffers)を safetensors で保存。
|
| 73 |
+
重要: float 限定で filter すると RoPE 等 integer buffer が落ちて model 壊れる。"""
|
| 74 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 75 |
+
cpu_sd = {k: v.detach().cpu().contiguous() for k, v in state_dict.items()
|
| 76 |
+
if isinstance(v, torch.Tensor)}
|
| 77 |
+
save_file(cpu_sd, str(path))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def main():
|
| 81 |
+
ap = argparse.ArgumentParser()
|
| 82 |
+
ap.add_argument("--phase", choices=["a", "b", "c"], required=True)
|
| 83 |
+
ap.add_argument("--dataset", required=True, type=str,
|
| 84 |
+
help="/dataset/raw のような image+caption ディレクトリ")
|
| 85 |
+
ap.add_argument("--out", required=True, type=str,
|
| 86 |
+
help="checkpoint 出力先ディレクトリ")
|
| 87 |
+
ap.add_argument("--resume", default=None, type=str,
|
| 88 |
+
help="前 phase の出力ディレクトリ (phase B/C で必須)")
|
| 89 |
+
ap.add_argument("--total-steps", type=int, default=3000)
|
| 90 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 91 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 92 |
+
ap.add_argument("--resolution", type=int, default=1024)
|
| 93 |
+
ap.add_argument("--num-workers", type=int, default=4)
|
| 94 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 95 |
+
ap.add_argument("--bf16", action="store_true", default=True)
|
| 96 |
+
ap.add_argument("--gen-lora-only", action="store_true", default=False,
|
| 97 |
+
help="gen にも Q,V LoRA を attach し base を凍結。Phase B/C 用。"
|
| 98 |
+
"出力は LoRA weights のみ (数百MB) になる。")
|
| 99 |
+
ap.add_argument("--override-adv-weight", type=float, default=-1.0,
|
| 100 |
+
help="PHASE_CONFIGS の adv_weight を上書き (−1 = 上書きしない)")
|
| 101 |
+
ap.add_argument("--override-r3gan-gamma", type=float, default=-1.0,
|
| 102 |
+
help="PHASE_CONFIGS の r3gan_gamma を上書き (−1 = 上書きしない)")
|
| 103 |
+
args = ap.parse_args()
|
| 104 |
+
|
| 105 |
+
torch.manual_seed(args.seed)
|
| 106 |
+
device = torch.device("cuda")
|
| 107 |
+
dtype = torch.bfloat16 if args.bf16 else torch.float32
|
| 108 |
+
|
| 109 |
+
out_dir = Path(args.out)
|
| 110 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 111 |
+
|
| 112 |
+
# ----- load Anima base -----
|
| 113 |
+
print("[load] Anima bundle (DiT + Qwen3 + WanVAE + LLMAdapter)")
|
| 114 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 115 |
+
|
| 116 |
+
# ----- gen / guidance を base から複製 -----
|
| 117 |
+
print("[setup] cloning DiT to gen + guidance")
|
| 118 |
+
gen_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype)
|
| 119 |
+
guidance_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype)
|
| 120 |
+
# bundle.transformer は推論用途で残してもよいが、メモリ節約のため None 化:
|
| 121 |
+
bundle.transformer = None
|
| 122 |
+
torch.cuda.empty_cache()
|
| 123 |
+
|
| 124 |
+
# ----- LoRA を guidance に attach -----
|
| 125 |
+
args_phase = PHASE_CONFIGS[args.phase]
|
| 126 |
+
print(f"[setup] attaching Q,V LoRA (rank={args_phase.lora_rank}) to guidance")
|
| 127 |
+
guidance_transformer = attach_qv_lora(guidance_transformer, rank=args_phase.lora_rank)
|
| 128 |
+
guidance_transformer.to(device=device, dtype=dtype)
|
| 129 |
+
|
| 130 |
+
# base 部分の grad を切る (LoRA だけ訓練)
|
| 131 |
+
for n, p in guidance_transformer.named_parameters():
|
| 132 |
+
p.requires_grad = ("lora_" in n)
|
| 133 |
+
|
| 134 |
+
# ----- resume (前 phase の gen 重みを新 base として引き継ぐ) -----
|
| 135 |
+
# Phase B/C は Phase A の蒸留済 DiT を base にする想定。LoRA-only 学習時は
|
| 136 |
+
# この段階で gen_transformer に Phase A の重みをロード → その後 LoRA attach + 凍結。
|
| 137 |
+
if args.resume:
|
| 138 |
+
resume_dir = Path(args.resume)
|
| 139 |
+
gen_ckpt = resume_dir / "gen_final.safetensors"
|
| 140 |
+
if gen_ckpt.exists():
|
| 141 |
+
print(f"[resume] loading gen from {gen_ckpt}")
|
| 142 |
+
from safetensors.torch import load_file
|
| 143 |
+
sd = load_file(str(gen_ckpt))
|
| 144 |
+
missing, unexpected = gen_transformer.load_state_dict(sd, strict=False)
|
| 145 |
+
print(f" missing={len(missing)} unexpected={len(unexpected)}")
|
| 146 |
+
|
| 147 |
+
# ----- CLI override for grid search / experimentation -----
|
| 148 |
+
if args.override_adv_weight >= 0:
|
| 149 |
+
args_phase.adv_weight = args.override_adv_weight
|
| 150 |
+
print(f"[override] adv_weight -> {args_phase.adv_weight}")
|
| 151 |
+
if args.override_r3gan_gamma >= 0:
|
| 152 |
+
args_phase.r3gan_gamma = args.override_r3gan_gamma
|
| 153 |
+
print(f"[override] r3gan_gamma -> {args_phase.r3gan_gamma}")
|
| 154 |
+
|
| 155 |
+
# ----- LoRA-only モード: gen に wide LoRA (all-linear) を attach + base 凍結 -----
|
| 156 |
+
# 注: Q,V LoRA だと AdaLN modulation を触れず、Anima のような timestep 依存
|
| 157 |
+
# architecture では蒸留信号が degenerate に収束する (Phase B v1 で失敗)。
|
| 158 |
+
# wide にして AdaLN + attention + MLP を全部学習対象に。
|
| 159 |
+
if args.gen_lora_only:
|
| 160 |
+
print(f"[setup] gen-lora-only mode: attaching WIDE LoRA (rank={args_phase.lora_rank}) to gen")
|
| 161 |
+
gen_transformer = attach_wide_lora(gen_transformer, rank=args_phase.lora_rank)
|
| 162 |
+
gen_transformer.to(device=device, dtype=dtype)
|
| 163 |
+
# base を凍結、LoRA だけ trainable
|
| 164 |
+
for n, p in gen_transformer.named_parameters():
|
| 165 |
+
p.requires_grad = ("lora_" in n)
|
| 166 |
+
trainable = sum(p.numel() for p in gen_transformer.parameters() if p.requires_grad)
|
| 167 |
+
total = sum(p.numel() for p in gen_transformer.parameters())
|
| 168 |
+
print(f" gen trainable params: {trainable/1e6:.1f}M / {total/1e6:.1f}M total")
|
| 169 |
+
|
| 170 |
+
# ----- discriminator -----
|
| 171 |
+
# adv_weight=0 のときは構築しない (Phase A 教訓: R3GAN 自体が不安定要因)
|
| 172 |
+
if args_phase.adv_weight > 0:
|
| 173 |
+
print("[setup] R3GAN discriminator (latent space, text-conditional)")
|
| 174 |
+
disc = R3GANDiscriminator(latent_channels=16, cond_dim=1024).to(device=device, dtype=dtype)
|
| 175 |
+
else:
|
| 176 |
+
print("[setup] R3GAN DISABLED (adv_weight=0) — DMD+TSCD のみで蒸留")
|
| 177 |
+
# placeholder: trainer は disc を受け取るが forward は呼ばれない
|
| 178 |
+
disc = R3GANDiscriminator(latent_channels=16, cond_dim=1024).to(device=device, dtype=dtype)
|
| 179 |
+
|
| 180 |
+
# ----- dataset -----
|
| 181 |
+
print(f"[data] loading {args.dataset}")
|
| 182 |
+
dataset = AnimaImageCaptionDataset(args.dataset, resolution=args.resolution)
|
| 183 |
+
print(f" {len(dataset)} samples")
|
| 184 |
+
loader = DataLoader(
|
| 185 |
+
dataset,
|
| 186 |
+
batch_size=args_phase.batch_size,
|
| 187 |
+
shuffle=True,
|
| 188 |
+
num_workers=args.num_workers,
|
| 189 |
+
collate_fn=collate_fn,
|
| 190 |
+
drop_last=True,
|
| 191 |
+
pin_memory=True,
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# ----- trainer -----
|
| 195 |
+
trainer = DMD2Trainer(
|
| 196 |
+
bundle=bundle,
|
| 197 |
+
gen_transformer=gen_transformer,
|
| 198 |
+
guidance_transformer=guidance_transformer,
|
| 199 |
+
discriminator=disc,
|
| 200 |
+
args=args_phase,
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
# ----- training loop -----
|
| 204 |
+
print(f"[train] phase={args.phase} steps={args.total_steps} batch={args_phase.batch_size}")
|
| 205 |
+
log_path = out_dir / f"phase_{args.phase}_log.jsonl"
|
| 206 |
+
log_f = open(log_path, "a", buffering=1)
|
| 207 |
+
t0 = time.time()
|
| 208 |
+
data_iter = iter(loader)
|
| 209 |
+
|
| 210 |
+
for step in range(args.total_steps):
|
| 211 |
+
try:
|
| 212 |
+
batch = next(data_iter)
|
| 213 |
+
except StopIteration:
|
| 214 |
+
data_iter = iter(loader)
|
| 215 |
+
batch = next(data_iter)
|
| 216 |
+
|
| 217 |
+
metrics = trainer.train_step(batch)
|
| 218 |
+
|
| 219 |
+
if step % args.log_every == 0:
|
| 220 |
+
scalars = {k: float(v) for k, v in metrics.items()}
|
| 221 |
+
scalars["step"] = step
|
| 222 |
+
scalars["elapsed"] = time.time() - t0
|
| 223 |
+
log_f.write(json.dumps(scalars) + "\n")
|
| 224 |
+
msg = " ".join(f"{k}={v:.4f}" for k, v in scalars.items() if k not in ("step", "elapsed"))
|
| 225 |
+
print(f"[step {step}/{args.total_steps}] elapsed={scalars['elapsed']:.0f}s | {msg}")
|
| 226 |
+
|
| 227 |
+
if step > 0 and step % args.sample_every == 0:
|
| 228 |
+
if args.gen_lora_only:
|
| 229 |
+
save_lora_state(trainer.gen, out_dir, f"phase_{args.phase}_step{step:05d}_gen_lora")
|
| 230 |
+
print(f"[save] phase_{args.phase}_step{step:05d}_gen_lora.safetensors", flush=True)
|
| 231 |
+
else:
|
| 232 |
+
ckpt = out_dir / f"phase_{args.phase}_step{step:05d}_gen.safetensors"
|
| 233 |
+
save_full_state(trainer.gen.state_dict(), ckpt)
|
| 234 |
+
print(f"[save] {ckpt}", flush=True)
|
| 235 |
+
# Modal volume の commit を内部で発行(途中クラッシュからの保全)
|
| 236 |
+
try:
|
| 237 |
+
import modal
|
| 238 |
+
vol = modal.Volume.from_name("anima-outputs")
|
| 239 |
+
vol.commit()
|
| 240 |
+
print(f"[save] volume committed at step {step}", flush=True)
|
| 241 |
+
except Exception as e:
|
| 242 |
+
print(f"[save] volume commit failed: {e}", flush=True)
|
| 243 |
+
|
| 244 |
+
# final save
|
| 245 |
+
print("[done] saving final outputs")
|
| 246 |
+
if args.gen_lora_only:
|
| 247 |
+
# LoRA-only: gen の LoRA weights だけ保存(数百 MB)
|
| 248 |
+
save_lora_state(trainer.gen, out_dir, "gen_lora_final")
|
| 249 |
+
print(f" -> gen_lora_final.safetensors (LoRA-only mode)")
|
| 250 |
+
else:
|
| 251 |
+
# Full DiT: gen の全 weights を保存(~4 GB)
|
| 252 |
+
save_full_state(trainer.gen.state_dict(), out_dir / "gen_final.safetensors")
|
| 253 |
+
print(f" -> gen_final.safetensors (full DiT)")
|
| 254 |
+
# guidance LoRA は両モード共通で残しておく(分析・実験用)
|
| 255 |
+
save_lora_state(trainer.guidance, out_dir, "guidance_lora_final")
|
| 256 |
+
log_f.close()
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
if __name__ == "__main__":
|
| 260 |
+
main()
|
scripts/distill/train_traj.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Anima trajectory imitation distillation
|
| 4 |
+
========================================
|
| 5 |
+
|
| 6 |
+
DiffSynth-Studio の Z-Image trajectory imitation を Anima に最小移植したもの。
|
| 7 |
+
**critic なし、単一ネットワーク**で前回 5 回失敗の R3GAN 不安定性を完全回避。
|
| 8 |
+
|
| 9 |
+
アルゴリズム:
|
| 10 |
+
- teacher = Anima base (frozen, no LoRA) を 50-step CFG=2.0 で rollout
|
| 11 |
+
- student = Anima base + wide LoRA (rank 32) を 8-step CFG=1.0 で rollout
|
| 12 |
+
- L_align = MSE(student velocity, teacher segment velocity) on 8 segments
|
| 13 |
+
- L_reg = LPIPS(student final image, teacher final image) (任意、--lpips-weight>0 で有効)
|
| 14 |
+
|
| 15 |
+
warm-start:
|
| 16 |
+
--warm-lora /models/loras/anima_turbo.safetensors で Civitai 公式 Anima Turbo LoRA を
|
| 17 |
+
student LoRA 初期値に注入できる。format は ComfyUI 形式 (diffusion_model.<...>.lora_A.weight)、
|
| 18 |
+
内部で PEFT 形式に変換してロード。
|
| 19 |
+
|
| 20 |
+
使い方 (Modal 経由):
|
| 21 |
+
# Smoke test (1 step, sanity check)
|
| 22 |
+
modal run modal_app.py::train_traj_imitation --total-steps 1 --batch-size 1 \\
|
| 23 |
+
--teacher-steps 12 --student-steps 8 --lpips-weight 0.0
|
| 24 |
+
|
| 25 |
+
# 本番 (2000 step, ~$45, B200)
|
| 26 |
+
modal run --detach modal_app.py::train_traj_imitation \\
|
| 27 |
+
--total-steps 2000 --batch-size 1 --teacher-steps 50 --student-steps 8 \\
|
| 28 |
+
--warm-lora /models/loras/anima_turbo.safetensors --lpips-weight 0.1
|
| 29 |
+
"""
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
import argparse
|
| 32 |
+
import copy
|
| 33 |
+
import json
|
| 34 |
+
import os
|
| 35 |
+
import sys
|
| 36 |
+
import time
|
| 37 |
+
from pathlib import Path
|
| 38 |
+
|
| 39 |
+
import torch
|
| 40 |
+
import torch.nn.functional as F
|
| 41 |
+
from torch.utils.data import DataLoader, Dataset
|
| 42 |
+
from safetensors.torch import save_file, load_file
|
| 43 |
+
|
| 44 |
+
# import-path: 同ディレクトリ前提
|
| 45 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 46 |
+
from distill.anima_loader import AnimaPaths, build_anima, AnimaBundle
|
| 47 |
+
from distill.dmd2_trainer import attach_wide_lora
|
| 48 |
+
from distill.traj_scheduler import make_schedule, snap_to_targets
|
| 49 |
+
from distill.traj_loss import (
|
| 50 |
+
fetch_trajectory, align_trajectory, compute_regularization,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
# Text-only dataset: caption だけ使う (trajectory imitation は init=noise なので image 不要)
|
| 56 |
+
# ---------------------------------------------------------------------------
|
| 57 |
+
class TextOnlyDataset(Dataset):
|
| 58 |
+
def __init__(self, root: str | Path):
|
| 59 |
+
self.root = Path(root)
|
| 60 |
+
# *.txt を再帰的に拾う。/dataset/cleaned 配下を想定。
|
| 61 |
+
self.files = sorted(self.root.rglob("*.txt"))
|
| 62 |
+
if not self.files:
|
| 63 |
+
raise RuntimeError(f"No .txt files under {self.root}")
|
| 64 |
+
|
| 65 |
+
def __len__(self) -> int:
|
| 66 |
+
return len(self.files)
|
| 67 |
+
|
| 68 |
+
def __getitem__(self, idx: int) -> str:
|
| 69 |
+
return self.files[idx].read_text(encoding="utf-8").strip()
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def text_collate(batch: list[str]) -> list[str]:
|
| 73 |
+
return list(batch)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
# ComfyUI Anima LoRA -> PEFT 形式変換 (warm-start 用)
|
| 78 |
+
# 既存の _convert_peft_to_comfy_lora の逆向き
|
| 79 |
+
# ---------------------------------------------------------------------------
|
| 80 |
+
def convert_comfy_to_peft_lora(sd: dict) -> dict:
|
| 81 |
+
"""ComfyUI(Anima/Cosmos)形式 LoRA を PEFT 形式に変換。
|
| 82 |
+
Comfy: 'diffusion_model.<module>.lora_A.weight'
|
| 83 |
+
PEFT: 'base_model.model.<module>.lora_A.default.weight'
|
| 84 |
+
"""
|
| 85 |
+
out = {}
|
| 86 |
+
for k, v in sd.items():
|
| 87 |
+
nk = k
|
| 88 |
+
if nk.startswith("diffusion_model."):
|
| 89 |
+
nk = nk[len("diffusion_model."):]
|
| 90 |
+
# adapter name '.default.' を挿入
|
| 91 |
+
nk = nk.replace(".lora_A.weight", ".lora_A.default.weight")
|
| 92 |
+
nk = nk.replace(".lora_B.weight", ".lora_B.default.weight")
|
| 93 |
+
nk = nk.replace(".lora_A.bias", ".lora_A.default.bias")
|
| 94 |
+
nk = nk.replace(".lora_B.bias", ".lora_B.default.bias")
|
| 95 |
+
nk = "base_model.model." + nk
|
| 96 |
+
out[nk] = v
|
| 97 |
+
return out
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def load_warm_lora(student_model, warm_lora_path: str) -> None:
|
| 101 |
+
"""Civitai 形式の Anima Turbo LoRA を student の PEFT 形式に変換して注入。
|
| 102 |
+
shape / rank が合わない key は skip して警告。"""
|
| 103 |
+
print(f"[warm] loading {warm_lora_path}")
|
| 104 |
+
sd_comfy = load_file(warm_lora_path)
|
| 105 |
+
print(f"[warm] {len(sd_comfy)} comfy keys, sample: {list(sd_comfy.keys())[:3]}")
|
| 106 |
+
sd_peft = convert_comfy_to_peft_lora(sd_comfy)
|
| 107 |
+
print(f"[warm] converted to {len(sd_peft)} peft keys, sample: {list(sd_peft.keys())[:3]}")
|
| 108 |
+
|
| 109 |
+
# student の現 state_dict と shape を照合して filter
|
| 110 |
+
model_sd = student_model.state_dict()
|
| 111 |
+
matched, skipped_shape, missing = 0, 0, 0
|
| 112 |
+
to_load = {}
|
| 113 |
+
for k, v in sd_peft.items():
|
| 114 |
+
if k not in model_sd:
|
| 115 |
+
missing += 1
|
| 116 |
+
continue
|
| 117 |
+
if model_sd[k].shape != v.shape:
|
| 118 |
+
skipped_shape += 1
|
| 119 |
+
continue
|
| 120 |
+
to_load[k] = v.to(dtype=model_sd[k].dtype)
|
| 121 |
+
matched += 1
|
| 122 |
+
print(f"[warm] matched={matched} skipped_shape={skipped_shape} missing={missing}")
|
| 123 |
+
if matched == 0:
|
| 124 |
+
raise RuntimeError(
|
| 125 |
+
"No LoRA keys matched. Check format conversion & target_modules of wide LoRA."
|
| 126 |
+
)
|
| 127 |
+
missing_keys, _ = student_model.load_state_dict(to_load, strict=False)
|
| 128 |
+
# PEFT 内部の LoRA キーだけ載れば OK、その他 missing は base 側なので無視
|
| 129 |
+
lora_missing = [k for k in missing_keys if "lora_" in k]
|
| 130 |
+
if lora_missing:
|
| 131 |
+
print(f"[warm] WARN: {len(lora_missing)} lora keys not loaded (e.g. {lora_missing[:3]})")
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ---------------------------------------------------------------------------
|
| 135 |
+
# save / restore
|
| 136 |
+
# ---------------------------------------------------------------------------
|
| 137 |
+
def save_lora_state(model, path: Path, name: str) -> None:
|
| 138 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 139 |
+
sd = {k: v.detach().cpu() for k, v in model.state_dict().items() if "lora_" in k}
|
| 140 |
+
save_file(sd, str(path / f"{name}.safetensors"))
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
# main
|
| 145 |
+
# ---------------------------------------------------------------------------
|
| 146 |
+
def main():
|
| 147 |
+
ap = argparse.ArgumentParser()
|
| 148 |
+
ap.add_argument("--dataset", required=True, type=str,
|
| 149 |
+
help="caption (*.txt) を含むディレクトリ")
|
| 150 |
+
ap.add_argument("--out", required=True, type=str,
|
| 151 |
+
help="LoRA 出力ディレクトリ")
|
| 152 |
+
ap.add_argument("--warm-lora", default="", type=str,
|
| 153 |
+
help="Civitai Anima Turbo LoRA path (ComfyUI 形式)。空なら cold-start")
|
| 154 |
+
ap.add_argument("--total-steps", type=int, default=2000)
|
| 155 |
+
ap.add_argument("--batch-size", type=int, default=1)
|
| 156 |
+
ap.add_argument("--teacher-steps", type=int, default=50)
|
| 157 |
+
ap.add_argument("--student-steps", type=int, default=8)
|
| 158 |
+
ap.add_argument("--teacher-cfg", type=float, default=2.0)
|
| 159 |
+
ap.add_argument("--student-cfg", type=float, default=1.0)
|
| 160 |
+
ap.add_argument("--sigma-shift", type=float, default=3.0,
|
| 161 |
+
help="Anima 公式 workflow は 3.0")
|
| 162 |
+
ap.add_argument("--lora-rank", type=int, default=32)
|
| 163 |
+
ap.add_argument("--lr", type=float, default=1e-4)
|
| 164 |
+
ap.add_argument("--weight-decay", type=float, default=0.01)
|
| 165 |
+
ap.add_argument("--grad-clip", type=float, default=1.0)
|
| 166 |
+
ap.add_argument("--lpips-weight", type=float, default=0.0,
|
| 167 |
+
help="LPIPS regularization weight。0 で disable (start 推奨)、後で 0.1-0.5 に")
|
| 168 |
+
ap.add_argument("--resolution", type=int, default=1024)
|
| 169 |
+
ap.add_argument("--log-every", type=int, default=10)
|
| 170 |
+
ap.add_argument("--sample-every", type=int, default=500)
|
| 171 |
+
ap.add_argument("--num-workers", type=int, default=2)
|
| 172 |
+
ap.add_argument("--seed", type=int, default=42)
|
| 173 |
+
ap.add_argument("--weight-mode", default="uniform", choices=["uniform", "inv_sigma"])
|
| 174 |
+
ap.add_argument("--neg-prompt", default="",
|
| 175 |
+
help="negative prompt for teacher CFG (空文字 = empty conditioning)")
|
| 176 |
+
args = ap.parse_args()
|
| 177 |
+
|
| 178 |
+
torch.manual_seed(args.seed)
|
| 179 |
+
device = torch.device("cuda")
|
| 180 |
+
dtype = torch.bfloat16
|
| 181 |
+
|
| 182 |
+
out_dir = Path(args.out)
|
| 183 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 184 |
+
|
| 185 |
+
# ----- load Anima base -----
|
| 186 |
+
print("[load] Anima bundle (DiT + Qwen3 + WanVAE + LLMAdapter)")
|
| 187 |
+
bundle = build_anima(device=device, dtype=dtype)
|
| 188 |
+
|
| 189 |
+
# ----- teacher = base のクローン (frozen, no LoRA) -----
|
| 190 |
+
print("[setup] cloning DiT for teacher (frozen) and student (wide LoRA)")
|
| 191 |
+
teacher_transformer = copy.deepcopy(bundle.transformer).to(device=device, dtype=dtype).eval()
|
| 192 |
+
for p in teacher_transformer.parameters():
|
| 193 |
+
p.requires_grad = False
|
| 194 |
+
|
| 195 |
+
# ----- student = base に wide LoRA を attach -----
|
| 196 |
+
student_transformer = bundle.transformer # bundle 側の方を再利用 (メモリ節約)
|
| 197 |
+
student_transformer = attach_wide_lora(student_transformer, rank=args.lora_rank)
|
| 198 |
+
student_transformer.to(device=device, dtype=dtype)
|
| 199 |
+
# base 凍結、LoRA だけ trainable
|
| 200 |
+
for n, p in student_transformer.named_parameters():
|
| 201 |
+
p.requires_grad = ("lora_" in n)
|
| 202 |
+
trainable = sum(p.numel() for p in student_transformer.parameters() if p.requires_grad)
|
| 203 |
+
total = sum(p.numel() for p in student_transformer.parameters())
|
| 204 |
+
print(f"[setup] student trainable: {trainable/1e6:.1f}M / {total/1e6:.1f}M")
|
| 205 |
+
# bundle.transformer は student と同じ object (LoRA wrap 済)。
|
| 206 |
+
# text_encode/vae_*以外には bundle.transformer 不要なので参照だけ残す。
|
| 207 |
+
bundle.transformer = student_transformer
|
| 208 |
+
|
| 209 |
+
# ----- warm-start (任意) -----
|
| 210 |
+
if args.warm_lora:
|
| 211 |
+
load_warm_lora(student_transformer, args.warm_lora)
|
| 212 |
+
|
| 213 |
+
# ----- LPIPS (任意) -----
|
| 214 |
+
lpips_fn = None
|
| 215 |
+
if args.lpips_weight > 0:
|
| 216 |
+
import lpips as _lpips
|
| 217 |
+
lpips_fn = _lpips.LPIPS(net="alex").to(device).eval()
|
| 218 |
+
for p in lpips_fn.parameters():
|
| 219 |
+
p.requires_grad = False
|
| 220 |
+
print("[setup] LPIPS(alex) loaded for regularization")
|
| 221 |
+
|
| 222 |
+
# ----- schedules -----
|
| 223 |
+
# student schedule: 推論時に使う 8-step grid (t=1 → 0)
|
| 224 |
+
student_sched = make_schedule(args.student_steps, args.sigma_shift, device=device, dtype=torch.float32)
|
| 225 |
+
student_ts = student_sched.timesteps # (N+1,)
|
| 226 |
+
print(f"[schedule] student t = {student_ts.tolist()}")
|
| 227 |
+
|
| 228 |
+
# ----- dataset -----
|
| 229 |
+
print(f"[data] loading {args.dataset}")
|
| 230 |
+
dataset = TextOnlyDataset(args.dataset)
|
| 231 |
+
print(f" {len(dataset)} captions")
|
| 232 |
+
loader = DataLoader(
|
| 233 |
+
dataset,
|
| 234 |
+
batch_size=args.batch_size,
|
| 235 |
+
shuffle=True,
|
| 236 |
+
num_workers=args.num_workers,
|
| 237 |
+
collate_fn=text_collate,
|
| 238 |
+
drop_last=True,
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# ----- optimizer -----
|
| 242 |
+
opt = torch.optim.AdamW(
|
| 243 |
+
[p for p in student_transformer.parameters() if p.requires_grad],
|
| 244 |
+
lr=args.lr, betas=(0.9, 0.999), weight_decay=args.weight_decay, eps=1e-8,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
# ----- training loop -----
|
| 248 |
+
print(f"[train] total_steps={args.total_steps} batch={args.batch_size} "
|
| 249 |
+
f"teacher={args.teacher_steps} student={args.student_steps} "
|
| 250 |
+
f"lpips_weight={args.lpips_weight}")
|
| 251 |
+
log_path = out_dir / "traj_log.jsonl"
|
| 252 |
+
log_f = open(log_path, "a", buffering=1)
|
| 253 |
+
t0 = time.time()
|
| 254 |
+
data_iter = iter(loader)
|
| 255 |
+
|
| 256 |
+
# student velocity_fn: バッチ次元 latents + (B,) timesteps + cond -> velocity
|
| 257 |
+
def _student_v(x, t, cond):
|
| 258 |
+
return AnimaBundle.dit_forward(student_transformer, x, t, cond)
|
| 259 |
+
|
| 260 |
+
def _teacher_v(x, t, cond):
|
| 261 |
+
return AnimaBundle.dit_forward(teacher_transformer, x, t, cond)
|
| 262 |
+
|
| 263 |
+
# negative cond は 1 度だけエンコード
|
| 264 |
+
with torch.no_grad():
|
| 265 |
+
cond_neg = bundle.text_encode([args.neg_prompt or ""]) # (1, 512, 1024)
|
| 266 |
+
# latent サイズは VAE 8x down 想定 (Anima WanVAE)
|
| 267 |
+
H_lat = args.resolution // 8
|
| 268 |
+
W_lat = args.resolution // 8
|
| 269 |
+
|
| 270 |
+
for step in range(args.total_steps):
|
| 271 |
+
try:
|
| 272 |
+
captions = next(data_iter)
|
| 273 |
+
except StopIteration:
|
| 274 |
+
data_iter = iter(loader)
|
| 275 |
+
captions = next(data_iter)
|
| 276 |
+
|
| 277 |
+
B = len(captions)
|
| 278 |
+
|
| 279 |
+
# text encode (positive)
|
| 280 |
+
with torch.no_grad():
|
| 281 |
+
cond_pos = bundle.text_encode(captions) # (B, 512, 1024)
|
| 282 |
+
# neg は B にブロードキャスト
|
| 283 |
+
cond_neg_b = cond_neg.expand(B, -1, -1).contiguous() if B > 1 else cond_neg
|
| 284 |
+
|
| 285 |
+
# 初期 noise (t=1 の状態)
|
| 286 |
+
init_noise = torch.randn(
|
| 287 |
+
B, 16, 1, H_lat, W_lat, device=device, dtype=dtype,
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# ----- teacher rollout (no_grad inside fetch_trajectory) -----
|
| 291 |
+
teacher_sched = make_schedule(args.teacher_steps, args.sigma_shift,
|
| 292 |
+
device=device, dtype=torch.float32)
|
| 293 |
+
teacher_sched = snap_to_targets(teacher_sched, student_ts)
|
| 294 |
+
teacher_transformer.eval()
|
| 295 |
+
teacher_traj = fetch_trajectory(
|
| 296 |
+
_teacher_v, init_noise, teacher_sched, student_ts,
|
| 297 |
+
cond_pos, cond_neg_b, cfg_scale=args.teacher_cfg,
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
# ----- student align loss -----
|
| 301 |
+
student_transformer.train()
|
| 302 |
+
l_align = align_trajectory(
|
| 303 |
+
_student_v, teacher_traj, student_ts,
|
| 304 |
+
cond_pos, cond_neg_b, cfg_scale_student=args.student_cfg,
|
| 305 |
+
weight_mode=args.weight_mode,
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# ----- LPIPS reg (任意) -----
|
| 309 |
+
if lpips_fn is not None:
|
| 310 |
+
l_reg = compute_regularization(
|
| 311 |
+
_student_v, bundle, init_noise,
|
| 312 |
+
teacher_final_latent=teacher_traj[-1],
|
| 313 |
+
student_timesteps=student_ts,
|
| 314 |
+
cond_pos=cond_pos, cond_neg=cond_neg_b,
|
| 315 |
+
lpips_fn=lpips_fn,
|
| 316 |
+
cfg_scale=args.student_cfg,
|
| 317 |
+
)
|
| 318 |
+
else:
|
| 319 |
+
l_reg = torch.zeros((), device=device)
|
| 320 |
+
|
| 321 |
+
loss = l_align + args.lpips_weight * l_reg
|
| 322 |
+
|
| 323 |
+
opt.zero_grad()
|
| 324 |
+
loss.backward()
|
| 325 |
+
torch.nn.utils.clip_grad_norm_(
|
| 326 |
+
[p for p in student_transformer.parameters() if p.requires_grad],
|
| 327 |
+
args.grad_clip,
|
| 328 |
+
)
|
| 329 |
+
opt.step()
|
| 330 |
+
|
| 331 |
+
# ----- log -----
|
| 332 |
+
if step % args.log_every == 0:
|
| 333 |
+
metrics = {
|
| 334 |
+
"step": step,
|
| 335 |
+
"elapsed": time.time() - t0,
|
| 336 |
+
"loss": float(loss.detach()),
|
| 337 |
+
"l_align": float(l_align.detach()),
|
| 338 |
+
"l_reg": float(l_reg.detach()),
|
| 339 |
+
}
|
| 340 |
+
log_f.write(json.dumps(metrics) + "\n")
|
| 341 |
+
msg = " ".join(f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}"
|
| 342 |
+
for k, v in metrics.items() if k != "step")
|
| 343 |
+
print(f"[step {step}/{args.total_steps}] {msg}", flush=True)
|
| 344 |
+
|
| 345 |
+
# ----- checkpoint -----
|
| 346 |
+
if step > 0 and step % args.sample_every == 0:
|
| 347 |
+
save_lora_state(student_transformer, out_dir, f"traj_step{step:05d}")
|
| 348 |
+
print(f"[save] traj_step{step:05d}.safetensors", flush=True)
|
| 349 |
+
try:
|
| 350 |
+
import modal
|
| 351 |
+
modal.Volume.from_name("anima-outputs").commit()
|
| 352 |
+
print(f"[save] volume committed at step {step}", flush=True)
|
| 353 |
+
except Exception as e:
|
| 354 |
+
print(f"[save] volume commit failed: {e}", flush=True)
|
| 355 |
+
|
| 356 |
+
# final
|
| 357 |
+
print("[done] saving final LoRA")
|
| 358 |
+
save_lora_state(student_transformer, out_dir, "traj_final")
|
| 359 |
+
log_f.close()
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
if __name__ == "__main__":
|
| 363 |
+
main()
|
scripts/distill/traj_loss.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Trajectory imitation losses for Anima distillation.
|
| 3 |
+
|
| 4 |
+
Algorithm (DiffSynth-Studio Z-Image trajectory_imitation 由来):
|
| 5 |
+
1. Teacher (= base Anima, frozen, no LoRA) を CFG=2.0 で 50 step rollout し
|
| 6 |
+
**student の 8 timesteps と一致する点** で latent を保存
|
| 7 |
+
→ teacher_traj = [x_at_t_1, x_at_t_2, ..., x_at_t_N] (N=8)
|
| 8 |
+
2. align loss: 各 student segment [t_i → t_{i+1}] で
|
| 9 |
+
target_v = (x_{i+1} - x_i) / (t_{i+1} - t_i)
|
| 10 |
+
student_v = student(x_i, t_i, cond)
|
| 11 |
+
L_align = MSE(student_v, target_v)
|
| 12 |
+
3. (optional) regularization: student が自前で 8-step rollout した最終画像と
|
| 13 |
+
teacher の最終画像を VAE decode して LPIPS で比較
|
| 14 |
+
|
| 15 |
+
R3GAN (Phase B 失敗の元凶) は**一切使わない**。critic なし、単一ネットワーク。
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
from typing import Callable
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
|
| 23 |
+
from .anima_loader import AnimaBundle
|
| 24 |
+
from .traj_scheduler import TrajSchedule
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ----- helpers -------------------------------------------------------------
|
| 28 |
+
|
| 29 |
+
def _broadcast_t(t: torch.Tensor, B: int, device, dtype) -> torch.Tensor:
|
| 30 |
+
"""scalar timestep を (B,) に膨らませる。"""
|
| 31 |
+
if t.dim() == 0:
|
| 32 |
+
t = t.expand(B)
|
| 33 |
+
return t.to(device=device, dtype=dtype)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def cfg_guided(
|
| 37 |
+
model_fn: Callable[..., torch.Tensor],
|
| 38 |
+
x: torch.Tensor,
|
| 39 |
+
t: torch.Tensor,
|
| 40 |
+
cond_pos: torch.Tensor,
|
| 41 |
+
cond_neg: torch.Tensor | None,
|
| 42 |
+
cfg_scale: float,
|
| 43 |
+
) -> torch.Tensor:
|
| 44 |
+
"""classifier-free guidance を 1 行で。cfg_scale<=1 なら neg を呼ばない。"""
|
| 45 |
+
v_pos = model_fn(x, t, cond_pos)
|
| 46 |
+
if cfg_scale <= 1.0 or cond_neg is None:
|
| 47 |
+
return v_pos
|
| 48 |
+
v_neg = model_fn(x, t, cond_neg)
|
| 49 |
+
return v_neg + cfg_scale * (v_pos - v_neg)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ----- teacher rollout (= "fetch trajectory") -------------------------------
|
| 53 |
+
|
| 54 |
+
@torch.no_grad()
|
| 55 |
+
def fetch_trajectory(
|
| 56 |
+
teacher_velocity_fn: Callable[..., torch.Tensor],
|
| 57 |
+
init_noise: torch.Tensor, # (B, 16, 1, H, W) at t=1
|
| 58 |
+
teacher_schedule: TrajSchedule, # snapped to student timesteps
|
| 59 |
+
student_timesteps: torch.Tensor, # (N+1,) student grid points
|
| 60 |
+
cond_pos: torch.Tensor,
|
| 61 |
+
cond_neg: torch.Tensor | None,
|
| 62 |
+
cfg_scale: float = 2.0,
|
| 63 |
+
) -> list[torch.Tensor]:
|
| 64 |
+
"""Teacher を CFG'd Euler で rollout し、student_timesteps と一致する点で
|
| 65 |
+
latent を保存して返す。
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
list of len(student_timesteps) tensors、それぞれ shape = init_noise.shape。
|
| 69 |
+
i 番目の要素が `latents at t == student_timesteps[i]`。
|
| 70 |
+
"""
|
| 71 |
+
B = init_noise.size(0)
|
| 72 |
+
device = init_noise.device
|
| 73 |
+
dtype = init_noise.dtype
|
| 74 |
+
|
| 75 |
+
ts = teacher_schedule.timesteps # (M+1,) — student 点を含む snap 済
|
| 76 |
+
student_set = {float(t.item()) for t in student_timesteps}
|
| 77 |
+
|
| 78 |
+
latents = init_noise
|
| 79 |
+
saved: dict[float, torch.Tensor] = {}
|
| 80 |
+
|
| 81 |
+
# 初期点 (t=1) が student に含まれていれば保存
|
| 82 |
+
if float(ts[0].item()) in student_set:
|
| 83 |
+
saved[float(ts[0].item())] = latents.detach().clone()
|
| 84 |
+
|
| 85 |
+
for i in range(len(ts) - 1):
|
| 86 |
+
t_cur = ts[i]
|
| 87 |
+
t_next = ts[i + 1]
|
| 88 |
+
t_in = _broadcast_t(t_cur, B, device, dtype)
|
| 89 |
+
v = cfg_guided(teacher_velocity_fn, latents, t_in, cond_pos, cond_neg, cfg_scale)
|
| 90 |
+
# Anima: euler step は forward 方向 (t は減少)
|
| 91 |
+
dt = (t_next - t_cur).to(device=device, dtype=dtype)
|
| 92 |
+
latents = latents + dt * v
|
| 93 |
+
if float(t_next.item()) in student_set:
|
| 94 |
+
saved[float(t_next.item())] = latents.detach().clone()
|
| 95 |
+
|
| 96 |
+
# student_timesteps の順序で並べて返す
|
| 97 |
+
return [saved[float(t.item())] for t in student_timesteps]
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ----- align loss (segment-wise velocity match) -----------------------------
|
| 101 |
+
|
| 102 |
+
def align_trajectory(
|
| 103 |
+
student_velocity_fn: Callable[..., torch.Tensor],
|
| 104 |
+
teacher_traj: list[torch.Tensor], # length N+1
|
| 105 |
+
student_timesteps: torch.Tensor, # (N+1,)
|
| 106 |
+
cond_pos: torch.Tensor,
|
| 107 |
+
cond_neg: torch.Tensor | None,
|
| 108 |
+
cfg_scale_student: float = 1.0,
|
| 109 |
+
weight_mode: str = "uniform",
|
| 110 |
+
) -> torch.Tensor:
|
| 111 |
+
"""各 segment ごとに student velocity を target velocity と MSE で揃える。
|
| 112 |
+
|
| 113 |
+
Anima 蒸留の核。grad は student の LoRA に流す前提なので no_grad で囲まない。
|
| 114 |
+
teacher_traj は detach 済 (fetch_trajectory が clone してある) なので
|
| 115 |
+
target に grad は乗らない。
|
| 116 |
+
"""
|
| 117 |
+
from .traj_scheduler import training_weight as _w
|
| 118 |
+
losses = []
|
| 119 |
+
weights = []
|
| 120 |
+
B = teacher_traj[0].size(0)
|
| 121 |
+
device = teacher_traj[0].device
|
| 122 |
+
dtype = teacher_traj[0].dtype
|
| 123 |
+
|
| 124 |
+
for i in range(len(student_timesteps) - 1):
|
| 125 |
+
t_cur = student_timesteps[i]
|
| 126 |
+
t_next = student_timesteps[i + 1]
|
| 127 |
+
x_cur = teacher_traj[i]
|
| 128 |
+
x_next = teacher_traj[i + 1]
|
| 129 |
+
dt = (t_next - t_cur).to(device=device, dtype=dtype)
|
| 130 |
+
# target velocity for this segment (teacher's effective velocity averaged over segment)
|
| 131 |
+
target_v = (x_next - x_cur) / dt
|
| 132 |
+
# student prediction at t_cur (greedy: assume student sees teacher's noisy state)
|
| 133 |
+
t_in = _broadcast_t(t_cur, B, device, dtype)
|
| 134 |
+
pred_v = cfg_guided(
|
| 135 |
+
student_velocity_fn, x_cur, t_in, cond_pos, cond_neg, cfg_scale_student
|
| 136 |
+
)
|
| 137 |
+
loss_i = F.mse_loss(pred_v.float(), target_v.detach().float())
|
| 138 |
+
losses.append(loss_i)
|
| 139 |
+
weights.append(_w(t_cur.unsqueeze(0).cpu(), mode=weight_mode).item())
|
| 140 |
+
|
| 141 |
+
losses_t = torch.stack(losses)
|
| 142 |
+
weights_t = torch.tensor(weights, device=losses_t.device, dtype=losses_t.dtype)
|
| 143 |
+
weights_t = weights_t / weights_t.sum().clamp(min=1e-8)
|
| 144 |
+
return (losses_t * weights_t).sum()
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ----- regularization (student own rollout + LPIPS vs teacher final) -------
|
| 148 |
+
|
| 149 |
+
def student_rollout(
|
| 150 |
+
student_velocity_fn: Callable[..., torch.Tensor],
|
| 151 |
+
init_noise: torch.Tensor,
|
| 152 |
+
student_timesteps: torch.Tensor,
|
| 153 |
+
cond_pos: torch.Tensor,
|
| 154 |
+
cond_neg: torch.Tensor | None,
|
| 155 |
+
cfg_scale: float = 1.0,
|
| 156 |
+
) -> torch.Tensor:
|
| 157 |
+
"""student が自前で N-step rollout し最終 latent を返す。**grad は通す**。"""
|
| 158 |
+
B = init_noise.size(0)
|
| 159 |
+
device = init_noise.device
|
| 160 |
+
dtype = init_noise.dtype
|
| 161 |
+
latents = init_noise
|
| 162 |
+
|
| 163 |
+
for i in range(len(student_timesteps) - 1):
|
| 164 |
+
t_cur = student_timesteps[i]
|
| 165 |
+
t_next = student_timesteps[i + 1]
|
| 166 |
+
t_in = _broadcast_t(t_cur, B, device, dtype)
|
| 167 |
+
v = cfg_guided(
|
| 168 |
+
student_velocity_fn, latents, t_in, cond_pos, cond_neg, cfg_scale
|
| 169 |
+
)
|
| 170 |
+
dt = (t_next - t_cur).to(device=device, dtype=dtype)
|
| 171 |
+
latents = latents + dt * v
|
| 172 |
+
return latents
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def compute_regularization(
|
| 176 |
+
student_velocity_fn: Callable[..., torch.Tensor],
|
| 177 |
+
bundle: AnimaBundle,
|
| 178 |
+
init_noise: torch.Tensor,
|
| 179 |
+
teacher_final_latent: torch.Tensor,
|
| 180 |
+
student_timesteps: torch.Tensor,
|
| 181 |
+
cond_pos: torch.Tensor,
|
| 182 |
+
cond_neg: torch.Tensor | None,
|
| 183 |
+
lpips_fn,
|
| 184 |
+
cfg_scale: float = 1.0,
|
| 185 |
+
) -> torch.Tensor:
|
| 186 |
+
"""LPIPS(VAE_decode(student_final), VAE_decode(teacher_final))。
|
| 187 |
+
student の rollout は grad-through。VAE は no_grad で固定 (decode 時のみ)。
|
| 188 |
+
"""
|
| 189 |
+
student_final = student_rollout(
|
| 190 |
+
student_velocity_fn, init_noise, student_timesteps,
|
| 191 |
+
cond_pos, cond_neg, cfg_scale,
|
| 192 |
+
)
|
| 193 |
+
# VAE decode: Anima は (B, 3, T=1, H, W) を返す → T を squeeze
|
| 194 |
+
# 注: vae_decode は @torch.no_grad で囲まれているため student grad が VAE 入力で切れる
|
| 195 |
+
# → LPIPS reg を活かすには別 forward が必要。ここでは bundle.vae.model.decode を直接
|
| 196 |
+
# 呼んで grad を通す (VAE 自体の weight は frozen のままだが入力勾配は流れる)。
|
| 197 |
+
vae_dtype = next(bundle.vae.model.parameters()).dtype
|
| 198 |
+
img_student = bundle.vae.model.decode(student_final.to(dtype=vae_dtype), bundle.vae_scale)
|
| 199 |
+
with torch.no_grad():
|
| 200 |
+
img_teacher = bundle.vae.model.decode(
|
| 201 |
+
teacher_final_latent.to(dtype=vae_dtype), bundle.vae_scale
|
| 202 |
+
)
|
| 203 |
+
# (B, 3, 1, H, W) -> (B, 3, H, W)、[-1, 1] のまま LPIPS に渡せる
|
| 204 |
+
img_student = img_student.squeeze(2)
|
| 205 |
+
img_teacher = img_teacher.squeeze(2)
|
| 206 |
+
# LPIPS は float32 を要求するので cast
|
| 207 |
+
return lpips_fn(img_student.float(), img_teacher.float()).mean()
|