IDM method for rapid_moss (data-space): DMD direction + L_pop moment matching, no adversarial
Browse files- training/rapid_moss_train_idm.py +577 -0
training/rapid_moss_train_idm.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Distillation of MOSS-Image-SFT to an R-step data-space generator (arXiv 2610.12102),
|
| 2 |
+
with method='idm' added by akhaliq (2026-10-31): IDM-style training (arXiv 2610.39633).
|
| 3 |
+
|
| 4 |
+
Based on darask0's rapid_moss/train.py (Apache-2.0,
|
| 5 |
+
hf.co/darask0/MOSS-Image_4-step_adapter_lora). The 'dmd' and 'dmad' methods are unchanged.
|
| 6 |
+
|
| 7 |
+
method='idm': no discriminator, no critic. Per iteration:
|
| 8 |
+
fake_per_gen fake-score updates (flow matching on generated x0, conditioned on r) [IDM L_fake]
|
| 9 |
+
one generator update with the DMD direction sg[v_real - v_fake] on x0 [IDM L_score]
|
| 10 |
+
plus L_pop: EMA feature moment matching (mean/std of last-block image-token features,
|
| 11 |
+
current sample + FIFO queue vs a reference queue built from teachergen samples) [IDM L_pop]
|
| 12 |
+
Deviation from the paper: the base loss is the data-space DMD fixed-point iteration
|
| 13 |
+
(arXiv 2610.12102) rather than RCGM; features come from the EMA generator path as in the paper.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import math
|
| 20 |
+
import os
|
| 21 |
+
import random
|
| 22 |
+
import time
|
| 23 |
+
from collections import deque
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import torch.distributed as dist
|
| 28 |
+
from torch.nn import functional as F
|
| 29 |
+
|
| 30 |
+
from .model import load_base
|
| 31 |
+
from .nets import Critic, Discriminator, FakeScore, Generator
|
| 32 |
+
from .textenc import Cache, read_prompts
|
| 33 |
+
|
| 34 |
+
DEFAULTS = {
|
| 35 |
+
'model_dir': '/workspace/models/MOSS-Image-SFT',
|
| 36 |
+
'lodestones_dir': '/workspace/models/MOSS-Image-SFT-8-steps-dmd',
|
| 37 |
+
'cache_dir': '/workspace/cache',
|
| 38 |
+
'prompts': '/workspace/prompts/train.jsonl',
|
| 39 |
+
'eval_prompts': '/workspace/prompts/eval.jsonl',
|
| 40 |
+
'out_dir': '/workspace/runs/main',
|
| 41 |
+
'steps': 4,
|
| 42 |
+
'gen_rank': 128, 'gen_alpha': 128,
|
| 43 |
+
'fake_rank': 64, 'fake_alpha': 64,
|
| 44 |
+
'disc_taps': [5, 11, 17, 23, 29, 35], 'disc_hidden': 256,
|
| 45 |
+
'iterations': 3000,
|
| 46 |
+
'micro': 2,
|
| 47 |
+
'fake_per_gen': 2,
|
| 48 |
+
'fake_warmup': 200,
|
| 49 |
+
'lr_gen': 1e-5, 'lr_time': 1e-5, 'lr_q': 1e-5, 'lr_fake': 1e-4, 'lr_disc': 5e-5,
|
| 50 |
+
'betas': [0.0, 0.99],
|
| 51 |
+
'ema': 0.999,
|
| 52 |
+
'cfg': [3.0, 3.0],
|
| 53 |
+
'skip_neg': [22],
|
| 54 |
+
'tau_beta': [2.5, 1.0], 'tau_clamp': [0.03, 0.985],
|
| 55 |
+
'dt': 1 / 64,
|
| 56 |
+
'adv_weight': 0.5, 'adv_start': 50, 'adv_warmup': 200,
|
| 57 |
+
'disc_real': 'adm', 'teacher_dir': '/workspace/teacher', 'p_gan': 0.5, 'gan_t': [0.02, 0.98],
|
| 58 |
+
'clip_gen': 0.0, 'clip_critic': 0.0,
|
| 59 |
+
'buckets': [[1024, 1024, 0.35], [896, 1152, 0.15], [832, 1216, 0.15], [1152, 896, 0.1],
|
| 60 |
+
[1216, 832, 0.1], [768, 1344, 0.075], [1344, 768, 0.075]],
|
| 61 |
+
'method': 'dmd',
|
| 62 |
+
'critic_rank': 128, 'critic_alpha': 128, 'critic_start': 21, 'critic_hidden': 512,
|
| 63 |
+
'critic_shift': 5.0, 'critic_sigma': [0.02, 0.98], 'lr_critic': 4e-5, 'wd_critic': 0.01,
|
| 64 |
+
'critic_warmup': 50,
|
| 65 |
+
# IDM (L_pop) settings
|
| 66 |
+
'lpop_weight': 0.5, 'lpop_queue': 64, 'lpop_sigma': [0.5, 0.98],
|
| 67 |
+
'ckpt': True,
|
| 68 |
+
'seed': 0,
|
| 69 |
+
'log_every': 10, 'sample_every': 100, 'save_every': 250, 'export_every': 500,
|
| 70 |
+
'eval_count': 8, 'eval_seed': 1234,
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def parse_config():
|
| 75 |
+
ap = argparse.ArgumentParser()
|
| 76 |
+
ap.add_argument('--config')
|
| 77 |
+
ap.add_argument('--set', nargs='*', default=[])
|
| 78 |
+
args = ap.parse_args()
|
| 79 |
+
cfg = dict(DEFAULTS)
|
| 80 |
+
if args.config:
|
| 81 |
+
cfg.update(json.loads(Path(args.config).read_text()))
|
| 82 |
+
for item in args.set:
|
| 83 |
+
key, value = item.split('=', 1)
|
| 84 |
+
if key not in DEFAULTS:
|
| 85 |
+
raise SystemExit(f'unknown key {key}')
|
| 86 |
+
try:
|
| 87 |
+
cfg[key] = json.loads(value)
|
| 88 |
+
except json.JSONDecodeError:
|
| 89 |
+
cfg[key] = value
|
| 90 |
+
return cfg
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class Dist:
|
| 94 |
+
def __init__(self):
|
| 95 |
+
self.on = 'RANK' in os.environ
|
| 96 |
+
if self.on:
|
| 97 |
+
dist.init_process_group('nccl')
|
| 98 |
+
self.rank = dist.get_rank() if self.on else 0
|
| 99 |
+
self.world = dist.get_world_size() if self.on else 1
|
| 100 |
+
self.device = torch.device('cuda', int(os.environ.get('LOCAL_RANK', 0)))
|
| 101 |
+
torch.cuda.set_device(self.device)
|
| 102 |
+
|
| 103 |
+
def mean_grads(self, params):
|
| 104 |
+
grads = [p.grad for p in params]
|
| 105 |
+
if self.world == 1:
|
| 106 |
+
return
|
| 107 |
+
flat = torch._utils._flatten_dense_tensors(grads)
|
| 108 |
+
dist.all_reduce(flat)
|
| 109 |
+
flat /= self.world
|
| 110 |
+
for g, f in zip(grads, torch._utils._unflatten_dense_tensors(flat, grads)):
|
| 111 |
+
g.copy_(f)
|
| 112 |
+
|
| 113 |
+
def mean(self, values):
|
| 114 |
+
t = torch.tensor(values, device=self.device, dtype=torch.float64)
|
| 115 |
+
if self.on:
|
| 116 |
+
dist.all_reduce(t)
|
| 117 |
+
t /= self.world
|
| 118 |
+
return t.tolist()
|
| 119 |
+
|
| 120 |
+
def barrier(self):
|
| 121 |
+
if self.on:
|
| 122 |
+
dist.barrier()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def zero_grads(params):
|
| 126 |
+
for p in params:
|
| 127 |
+
if p.grad is None:
|
| 128 |
+
p.grad = torch.zeros_like(p)
|
| 129 |
+
else:
|
| 130 |
+
p.grad.zero_()
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def load_teacher(dirs, rows, dev):
|
| 134 |
+
"""Scan teachergen shards; returns the prompt rows that have a teacher latent
|
| 135 |
+
(with w/h from the latent shape) and a getter id -> [1, C, h, w] fp32 on dev."""
|
| 136 |
+
from safetensors import safe_open
|
| 137 |
+
files, where = [], {}
|
| 138 |
+
for d in [dirs] if isinstance(dirs, str) else dirs:
|
| 139 |
+
for path in sorted(Path(d).glob('shard-*.safetensors')):
|
| 140 |
+
try:
|
| 141 |
+
f = safe_open(path, 'pt')
|
| 142 |
+
except Exception as e:
|
| 143 |
+
print(json.dumps({'skip_shard': str(path), 'error': str(e)[:120]}), flush=True)
|
| 144 |
+
continue
|
| 145 |
+
files.append(f)
|
| 146 |
+
for k in f.keys():
|
| 147 |
+
_, h, w = f.get_slice(k).get_shape()
|
| 148 |
+
where[k] = (len(files) - 1, w * 16, h * 16)
|
| 149 |
+
pool = [dict(r, w=where[r['id']][1], h=where[r['id']][2]) for r in rows if r['id'] in where]
|
| 150 |
+
|
| 151 |
+
def get(key):
|
| 152 |
+
return files[where[key][0]].get_tensor(key)[None].to(dev, torch.float32)
|
| 153 |
+
return pool, get
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def decode(vae, x0):
|
| 157 |
+
z = x0.to(vae.dtype)
|
| 158 |
+
bn, eps = vae.bn, vae.config.batch_norm_eps
|
| 159 |
+
z = z * (bn.running_var + eps).sqrt().view(1, -1, 1, 1) + bn.running_mean.view(1, -1, 1, 1)
|
| 160 |
+
b, c, h, w = z.shape
|
| 161 |
+
z = z.reshape(b, c // 4, 2, 2, h, w).permute(0, 1, 4, 2, 5, 3).reshape(b, c // 4, 2 * h, 2 * w)
|
| 162 |
+
img = vae.decode(z, return_dict=False)[0].clamp(-1, 1)
|
| 163 |
+
return ((img[0].float() + 1) * 127.5).round().byte().permute(1, 2, 0).cpu().numpy()
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def contact_sheet(images, path, cell=384, cols=4):
|
| 167 |
+
from PIL import Image
|
| 168 |
+
rows = math.ceil(len(images) / cols)
|
| 169 |
+
sheet = Image.new('RGB', (cols * cell, rows * cell), 'white')
|
| 170 |
+
for i, im in enumerate(images):
|
| 171 |
+
im = Image.fromarray(im)
|
| 172 |
+
im.thumbnail((cell, cell))
|
| 173 |
+
sheet.paste(im, ((i % cols) * cell + (cell - im.width) // 2, (i // cols) * cell + (cell - im.height) // 2))
|
| 174 |
+
sheet.save(path, quality=90)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def main():
|
| 178 |
+
C = parse_config()
|
| 179 |
+
D = Dist()
|
| 180 |
+
dev = D.device
|
| 181 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 182 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 183 |
+
out = Path(C['out_dir'])
|
| 184 |
+
if D.rank == 0:
|
| 185 |
+
(out / 'samples').mkdir(parents=True, exist_ok=True)
|
| 186 |
+
(out / 'ckpt').mkdir(exist_ok=True)
|
| 187 |
+
(out / 'config.json').write_text(json.dumps(C, indent=1))
|
| 188 |
+
R = C['steps']
|
| 189 |
+
|
| 190 |
+
model = load_base(C['model_dir'], dev)
|
| 191 |
+
gen = Generator(model, C['gen_rank'], C['gen_alpha'], R).to(dev)
|
| 192 |
+
gen.load_lodestones(C['lodestones_dir'])
|
| 193 |
+
ema = Generator(model, C['gen_rank'], C['gen_alpha'], R).to(dev).requires_grad_(False)
|
| 194 |
+
ema.load_state_dict(gen.state_dict())
|
| 195 |
+
betas = tuple(C['betas'])
|
| 196 |
+
gen_params = list(gen.parameters())
|
| 197 |
+
opt_gen = torch.optim.AdamW([{'params': list(gen.lora.parameters()), 'lr': C['lr_gen']},
|
| 198 |
+
{'params': list(gen.time.parameters()), 'lr': C['lr_time']},
|
| 199 |
+
{'params': [gen.Q], 'lr': C['lr_q']}], betas=betas, weight_decay=0.0)
|
| 200 |
+
parts = {'gen': gen, 'ema': ema, 'opt_gen': opt_gen}
|
| 201 |
+
dmad = C['method'] == 'dmad'
|
| 202 |
+
idm = C['method'] == 'idm'
|
| 203 |
+
if dmad:
|
| 204 |
+
critic = Critic(model, C['critic_rank'], C['critic_alpha'], C['critic_start'], C['critic_hidden']).to(dev)
|
| 205 |
+
opt_critic = torch.optim.AdamW(critic.parameters(), lr=C['lr_critic'], betas=betas,
|
| 206 |
+
weight_decay=C['wd_critic'])
|
| 207 |
+
parts.update(critic=critic, opt_critic=opt_critic)
|
| 208 |
+
trainables = (gen_params, list(critic.parameters()))
|
| 209 |
+
elif C['method'] == 'dmd':
|
| 210 |
+
fake = FakeScore(model, C['fake_rank'], C['fake_alpha'], R).to(dev)
|
| 211 |
+
disc = Discriminator(model.dim, C['disc_taps'], C['disc_hidden']).to(dev)
|
| 212 |
+
opt_fake = torch.optim.AdamW(fake.parameters(), lr=C['lr_fake'], betas=betas, weight_decay=0.0)
|
| 213 |
+
opt_disc = torch.optim.AdamW(disc.parameters(), lr=C['lr_disc'], betas=betas, weight_decay=0.0)
|
| 214 |
+
parts.update(fake=fake, disc=disc, opt_fake=opt_fake, opt_disc=opt_disc)
|
| 215 |
+
trainables = (gen_params, list(fake.parameters()), list(disc.parameters()))
|
| 216 |
+
elif idm:
|
| 217 |
+
fake = FakeScore(model, C['fake_rank'], C['fake_alpha'], R).to(dev)
|
| 218 |
+
opt_fake = torch.optim.AdamW(fake.parameters(), lr=C['lr_fake'], betas=betas, weight_decay=0.0)
|
| 219 |
+
parts.update(fake=fake, opt_fake=opt_fake)
|
| 220 |
+
trainables = (gen_params, list(fake.parameters()))
|
| 221 |
+
else:
|
| 222 |
+
raise SystemExit(f"method must be dmd, dmad or idm, got {C['method']}")
|
| 223 |
+
for params in trainables:
|
| 224 |
+
zero_grads(params)
|
| 225 |
+
|
| 226 |
+
cache = Cache(C['cache_dir'])
|
| 227 |
+
rows = [r for r in read_prompts([C['prompts']]) if r['id'] in cache]
|
| 228 |
+
evals = [r for r in read_prompts([C['eval_prompts']]) if r['id'] in cache][:C['eval_count']]
|
| 229 |
+
neg = model.context(cache.get('').to(dev))
|
| 230 |
+
buckets = C['buckets']
|
| 231 |
+
teacher = None
|
| 232 |
+
if dmad or idm or C['disc_real'] == 'teacher':
|
| 233 |
+
trows, teacher = load_teacher(C['teacher_dir'], rows, dev)
|
| 234 |
+
if D.rank == 0:
|
| 235 |
+
print(json.dumps({'teacher_samples': len(trows)}), flush=True)
|
| 236 |
+
elif C['disc_real'] != 'adm':
|
| 237 |
+
raise SystemExit(f"disc_real must be adm or teacher, got {C['disc_real']}")
|
| 238 |
+
|
| 239 |
+
start, gen_updates = 0, 0
|
| 240 |
+
latest = out / 'ckpt' / 'latest.pt'
|
| 241 |
+
if latest.exists():
|
| 242 |
+
state = torch.load(latest, map_location=dev, weights_only=False)
|
| 243 |
+
for key, mod in parts.items():
|
| 244 |
+
mod.load_state_dict(state[key])
|
| 245 |
+
start, gen_updates = state['it'], state['gen_updates']
|
| 246 |
+
del state
|
| 247 |
+
if D.rank == 0:
|
| 248 |
+
print(json.dumps({'resume': start, 'gen_updates': gen_updates}), flush=True)
|
| 249 |
+
rng = random.Random(C['seed'] * 1_000_003 + D.rank * 7919 + start)
|
| 250 |
+
torch.manual_seed(C['seed'] * 1_000_003 + D.rank * 7919 + start)
|
| 251 |
+
beta = torch.distributions.Beta(*[torch.tensor(float(v)) for v in C['tau_beta']])
|
| 252 |
+
lo, hi = C['tau_clamp']
|
| 253 |
+
dt = C['dt']
|
| 254 |
+
vae = None
|
| 255 |
+
|
| 256 |
+
def draw(pool=None):
|
| 257 |
+
pool = pool or rows
|
| 258 |
+
row = pool[rng.randrange(len(pool))]
|
| 259 |
+
if 'w' in row and 'h' in row:
|
| 260 |
+
w, h = row['w'], row['h']
|
| 261 |
+
else:
|
| 262 |
+
w, h, _ = rng.choices(buckets, weights=[b[2] for b in buckets])[0]
|
| 263 |
+
ctx = model.context(cache.get(row['id']).to(dev))
|
| 264 |
+
z = torch.randn(1, model.tr.config.latent_channels, h // 16, w // 16, device=dev)
|
| 265 |
+
return ctx, z, row
|
| 266 |
+
|
| 267 |
+
def tau():
|
| 268 |
+
return float(beta.sample().clamp(lo, hi))
|
| 269 |
+
|
| 270 |
+
def r_index(phase, m):
|
| 271 |
+
return ((phase * D.world + D.rank) * C['micro'] + m) % R
|
| 272 |
+
|
| 273 |
+
# ---- IDM L_pop reference: moment stats of EMA features over teachergen samples ----
|
| 274 |
+
ref_mean = ref_std = None
|
| 275 |
+
pop_q = deque(maxlen=C['lpop_queue'])
|
| 276 |
+
if idm:
|
| 277 |
+
last = model.depth - 1
|
| 278 |
+
ref_rows = rng.sample(trows, min(C['lpop_queue'], len(trows)))
|
| 279 |
+
feats = []
|
| 280 |
+
with torch.no_grad():
|
| 281 |
+
for row in ref_rows:
|
| 282 |
+
ctx = model.context(cache.get(row['id']).to(dev))
|
| 283 |
+
x_t = teacher(row['id'])
|
| 284 |
+
s = rng.uniform(*C['lpop_sigma'])
|
| 285 |
+
xt = (1 - s) * x_t + s * torch.randn_like(x_t)
|
| 286 |
+
f = model(xt, s * 1000, ctx, lora=ema.lora, tp=ema.time, taps=(last,))[last]
|
| 287 |
+
feats.append(f.float().mean(1)[0].cpu())
|
| 288 |
+
ref = torch.stack(feats)
|
| 289 |
+
ref_mean, ref_std = ref.mean(0).to(dev), ref.std(0).to(dev)
|
| 290 |
+
if D.rank == 0:
|
| 291 |
+
print(json.dumps({'lpop_reference': len(feats)}), flush=True)
|
| 292 |
+
|
| 293 |
+
def fake_step(phase):
|
| 294 |
+
losses = []
|
| 295 |
+
gen.lora.freeze_cache()
|
| 296 |
+
for m in range(C['micro']):
|
| 297 |
+
ctx, z, _ = draw()
|
| 298 |
+
r = r_index(phase, m)
|
| 299 |
+
x0 = gen.rollout(model, z, ctx, r + 1)
|
| 300 |
+
t = tau()
|
| 301 |
+
eps = torch.randn_like(z)
|
| 302 |
+
v = fake(model, (1 - t) * x0 + t * eps, t * 1000, ctx, r, ckpt=C['ckpt'])
|
| 303 |
+
loss = F.mse_loss(v, eps - x0)
|
| 304 |
+
(loss / C['micro']).backward()
|
| 305 |
+
losses.append(loss.item())
|
| 306 |
+
gen.lora.drop_cache()
|
| 307 |
+
D.mean_grads(list(fake.parameters()))
|
| 308 |
+
opt_fake.step()
|
| 309 |
+
opt_fake.zero_grad(set_to_none=False)
|
| 310 |
+
return sum(losses) / len(losses)
|
| 311 |
+
|
| 312 |
+
def gen_step_idm(phase):
|
| 313 |
+
"""IDM generator step: DMD direction + L_pop moment matching. No adversarial losses."""
|
| 314 |
+
stats = {k: 0.0 for k in ('dmd_rms', 'lpop', 'lpop_std_gap')}
|
| 315 |
+
fake.lora.freeze_cache()
|
| 316 |
+
gen.lora.freeze_cache()
|
| 317 |
+
last = model.depth - 1
|
| 318 |
+
for m in range(C['micro']):
|
| 319 |
+
ctx, z, row = draw(trows)
|
| 320 |
+
r = r_index(phase, m)
|
| 321 |
+
x_prev = gen.rollout(model, z, ctx, r)
|
| 322 |
+
x0_hat = gen.step(model, z, x_prev, r, ctx, ckpt=C['ckpt'])
|
| 323 |
+
x0d = x0_hat.detach()
|
| 324 |
+
t = tau()
|
| 325 |
+
eps = torch.randn_like(z)
|
| 326 |
+
xt = (1 - t) * x0d + t * eps
|
| 327 |
+
w = rng.uniform(*C['cfg'])
|
| 328 |
+
with torch.no_grad():
|
| 329 |
+
v_real = model.teacher(xt, t * 1000, ctx, neg, w, tuple(C['skip_neg']))
|
| 330 |
+
v_fake = fake(model, xt, t * 1000, ctx, r)
|
| 331 |
+
g_dmd = (v_real - v_fake) / x0d.numel()
|
| 332 |
+
stats['dmd_rms'] += (v_real - v_fake).square().mean().sqrt().item()
|
| 333 |
+
# L_pop: EMA feature moment matching; gradients flow through the frozen
|
| 334 |
+
# EMA net into x0_hat only (IDM paper, feature-moment matching).
|
| 335 |
+
s = rng.uniform(*C['lpop_sigma'])
|
| 336 |
+
eps2 = torch.randn_like(z)
|
| 337 |
+
f = model((1 - s) * x0_hat + s * eps2, s * 1000, ctx,
|
| 338 |
+
lora=ema.lora, tp=ema.time, taps=(last,), ckpt=C['ckpt'])[last]
|
| 339 |
+
v = f.float().mean(1)[0] # [D]
|
| 340 |
+
pool = torch.stack([v] + list(pop_q)) if pop_q else v[None]
|
| 341 |
+
mean_g, std_g = pool.mean(0), pool.std(0, unbiased=False) # n=1 on first step: unbiased std is NaN
|
| 342 |
+
loss_pop = (mean_g - ref_mean).square().mean() + (std_g - ref_std).square().mean()
|
| 343 |
+
g_pop, = torch.autograd.grad(C['lpop_weight'] * loss_pop, x0_hat)
|
| 344 |
+
pop_q.append(v.detach())
|
| 345 |
+
stats['lpop'] += loss_pop.item()
|
| 346 |
+
stats['lpop_std_gap'] += (std_g - ref_std).abs().mean().item()
|
| 347 |
+
x0_hat.backward((g_dmd + g_pop) / C['micro'])
|
| 348 |
+
del x0_hat
|
| 349 |
+
gen.lora.drop_cache()
|
| 350 |
+
fake.lora.drop_cache()
|
| 351 |
+
D.mean_grads(gen_params)
|
| 352 |
+
gnorm = torch.nn.utils.clip_grad_norm_(gen_params, C['clip_gen'] or 1e12)
|
| 353 |
+
if torch.isfinite(gnorm):
|
| 354 |
+
opt_gen.step()
|
| 355 |
+
with torch.no_grad():
|
| 356 |
+
for pe, p in zip(ema.parameters(), gen.parameters()):
|
| 357 |
+
pe.lerp_(p, 1 - C['ema'])
|
| 358 |
+
opt_gen.zero_grad(set_to_none=False)
|
| 359 |
+
stats['gnorm'] = gnorm.item()
|
| 360 |
+
return {k: v / C['micro'] for k, v in stats.items()}
|
| 361 |
+
|
| 362 |
+
def gen_step(phase):
|
| 363 |
+
ramp = 0.0 if gen_updates < C['adv_start'] else min(1.0, (gen_updates - C['adv_start'] + 1) / max(1, C['adv_warmup']))
|
| 364 |
+
stats = {k: 0.0 for k in ('dmd_rms', 'adv_ratio', 'lam', 'd_loss', 'd_real', 'd_fake', 'd_acc')}
|
| 365 |
+
n_gan = 0
|
| 366 |
+
fake.lora.freeze_cache()
|
| 367 |
+
gen.lora.freeze_cache()
|
| 368 |
+
for m in range(C['micro']):
|
| 369 |
+
use_teacher = teacher is not None and rng.random() < C['p_gan']
|
| 370 |
+
ctx, z, row = draw(trows if use_teacher else None)
|
| 371 |
+
r = r_index(phase, m)
|
| 372 |
+
x_prev = gen.rollout(model, z, ctx, r)
|
| 373 |
+
x0_hat = gen.step(model, z, x_prev, r, ctx, ckpt=C['ckpt'])
|
| 374 |
+
x0d = x0_hat.detach()
|
| 375 |
+
t = tau()
|
| 376 |
+
eps = torch.randn_like(z)
|
| 377 |
+
xt = (1 - t) * x0d + t * eps
|
| 378 |
+
w = rng.uniform(*C['cfg'])
|
| 379 |
+
with torch.no_grad():
|
| 380 |
+
v_real = model.teacher(xt, t * 1000, ctx, neg, w, tuple(C['skip_neg']))
|
| 381 |
+
v_fake = fake(model, xt, t * 1000, ctx, r)
|
| 382 |
+
g_dmd = (v_real - v_fake) / x0d.numel()
|
| 383 |
+
stats['dmd_rms'] += (v_real - v_fake).square().mean().sqrt().item()
|
| 384 |
+
_, _, h, wd = z.shape
|
| 385 |
+
leaf = x0d.clone().requires_grad_(True)
|
| 386 |
+
if teacher is None:
|
| 387 |
+
s = t - dt
|
| 388 |
+
x_fake = (1 - t) * leaf + t * eps - dt * v_fake
|
| 389 |
+
x_real = xt - dt * v_real
|
| 390 |
+
elif use_teacher:
|
| 391 |
+
s = rng.uniform(*C['gan_t'])
|
| 392 |
+
x_fake = (1 - s) * leaf + s * torch.randn_like(z)
|
| 393 |
+
x_real = (1 - s) * teacher(row['id']) + s * torch.randn_like(z)
|
| 394 |
+
else:
|
| 395 |
+
x0_hat.backward(g_dmd / C['micro'])
|
| 396 |
+
del x0_hat
|
| 397 |
+
continue
|
| 398 |
+
n_gan += 1
|
| 399 |
+
feats = model(x_fake, s * 1000, ctx, taps=disc.taps, ckpt=C['ckpt'])
|
| 400 |
+
logits_f = disc(feats, h, wd)
|
| 401 |
+
g_adv, = torch.autograd.grad(-logits_f.mean(), leaf)
|
| 402 |
+
ratio = (g_adv.norm() / (g_dmd.norm() + 1e-30)).item()
|
| 403 |
+
lam = C['adv_weight'] * ramp / max(ratio, 1e-12)
|
| 404 |
+
stats['adv_ratio'] += ratio
|
| 405 |
+
stats['lam'] += lam
|
| 406 |
+
x0_hat.backward((g_dmd + lam * g_adv) / C['micro'])
|
| 407 |
+
f_fake = {k: v.detach() for k, v in feats.items()}
|
| 408 |
+
del x0_hat, feats, logits_f
|
| 409 |
+
with torch.no_grad():
|
| 410 |
+
f_real = model(x_real, s * 1000, ctx, taps=disc.taps)
|
| 411 |
+
d_real, d_fake = disc(f_real, h, wd), disc(f_fake, h, wd)
|
| 412 |
+
d_loss = F.relu(1 - d_real).mean() + F.relu(1 + d_fake).mean()
|
| 413 |
+
(d_loss / C['micro']).backward()
|
| 414 |
+
stats['d_loss'] += d_loss.item()
|
| 415 |
+
stats['d_real'] += d_real.mean().item()
|
| 416 |
+
stats['d_fake'] += d_fake.mean().item()
|
| 417 |
+
stats['d_acc'] += 0.5 * ((d_real > 0).float().mean() + (d_fake < 0).float().mean()).item()
|
| 418 |
+
gen.lora.drop_cache()
|
| 419 |
+
fake.lora.drop_cache()
|
| 420 |
+
D.mean_grads(gen_params)
|
| 421 |
+
D.mean_grads(list(disc.parameters()))
|
| 422 |
+
gnorm = torch.nn.utils.clip_grad_norm_(gen_params, C['clip_gen'] or 1e12)
|
| 423 |
+
if torch.isfinite(gnorm):
|
| 424 |
+
opt_gen.step()
|
| 425 |
+
opt_disc.step()
|
| 426 |
+
opt_gen.zero_grad(set_to_none=False)
|
| 427 |
+
opt_disc.zero_grad(set_to_none=False)
|
| 428 |
+
with torch.no_grad():
|
| 429 |
+
for pe, p in zip(ema.parameters(), gen.parameters()):
|
| 430 |
+
pe.lerp_(p, 1 - C['ema'])
|
| 431 |
+
stats = {k: v / (C['micro'] if k == 'dmd_rms' else max(n_gan, 1)) for k, v in stats.items()}
|
| 432 |
+
stats['gan_frac'] = n_gan / C['micro']
|
| 433 |
+
stats['gnorm'] = gnorm.item()
|
| 434 |
+
stats['ramp'] = ramp
|
| 435 |
+
return stats
|
| 436 |
+
|
| 437 |
+
def critic_sigma():
|
| 438 |
+
u, k = rng.uniform(*C['critic_sigma']), C['critic_shift']
|
| 439 |
+
return k * u / (1 + (k - 1) * u)
|
| 440 |
+
|
| 441 |
+
def dmad_step(phase, gen_on):
|
| 442 |
+
stats = {k: 0.0 for k in ('g_logit', 'c_loss', 'd_teacher', 'd_gen', 'c_acc')}
|
| 443 |
+
stash = []
|
| 444 |
+
gen.lora.freeze_cache()
|
| 445 |
+
critic.requires_grad_(False)
|
| 446 |
+
for m in range(C['micro']):
|
| 447 |
+
ctx, z, row = draw(trows)
|
| 448 |
+
r = r_index(phase, m)
|
| 449 |
+
if gen_on:
|
| 450 |
+
x0_hat = gen.step(model, z, gen.rollout(model, z, ctx, r), r, ctx, ckpt=C['ckpt'])
|
| 451 |
+
s = critic_sigma()
|
| 452 |
+
logit = critic(model, (1 - s) * x0_hat + s * torch.randn_like(z), s * 1000, ctx, ckpt=C['ckpt'])
|
| 453 |
+
(-logit.mean() / C['micro']).backward()
|
| 454 |
+
stats['g_logit'] += logit.item()
|
| 455 |
+
x0 = x0_hat.detach()
|
| 456 |
+
del x0_hat, logit
|
| 457 |
+
else:
|
| 458 |
+
x0 = gen.rollout(model, z, ctx, r + 1)
|
| 459 |
+
stash.append((ctx, row, x0))
|
| 460 |
+
gen.lora.drop_cache()
|
| 461 |
+
if gen_on:
|
| 462 |
+
D.mean_grads(gen_params)
|
| 463 |
+
gnorm = torch.nn.utils.clip_grad_norm_(gen_params, C['clip_gen'] or 1e12)
|
| 464 |
+
if torch.isfinite(gnorm):
|
| 465 |
+
opt_gen.step()
|
| 466 |
+
with torch.no_grad():
|
| 467 |
+
for pe, p in zip(ema.parameters(), gen.parameters()):
|
| 468 |
+
pe.lerp_(p, 1 - C['ema'])
|
| 469 |
+
stats['gnorm'] = gnorm.item() * C['micro']
|
| 470 |
+
else:
|
| 471 |
+
stats['skip_gen'] = C['micro']
|
| 472 |
+
opt_gen.zero_grad(set_to_none=False)
|
| 473 |
+
critic.requires_grad_(True)
|
| 474 |
+
for ctx, row, x0 in stash:
|
| 475 |
+
s = critic_sigma()
|
| 476 |
+
d_q = critic(model, (1 - s) * teacher(row['id']) + s * torch.randn_like(x0), s * 1000, ctx, ckpt=C['ckpt'])
|
| 477 |
+
d_g = critic(model, (1 - s) * x0 + s * torch.randn_like(x0), s * 1000, ctx, ckpt=C['ckpt'])
|
| 478 |
+
loss = F.softplus(-d_q).mean() + F.softplus(d_g).mean()
|
| 479 |
+
(loss / C['micro']).backward()
|
| 480 |
+
stats['c_loss'] += loss.item()
|
| 481 |
+
stats['d_teacher'] += d_q.item()
|
| 482 |
+
stats['d_gen'] += d_g.item()
|
| 483 |
+
stats['c_acc'] += 0.5 * ((d_q > 0).float() + (d_g < 0).float()).item()
|
| 484 |
+
critic_params = list(critic.parameters())
|
| 485 |
+
D.mean_grads(critic_params)
|
| 486 |
+
cnorm = torch.nn.utils.clip_grad_norm_(critic_params, C['clip_critic'] or 1e12)
|
| 487 |
+
if torch.isfinite(cnorm):
|
| 488 |
+
opt_critic.step()
|
| 489 |
+
stats['cnorm'] = cnorm.item() * C['micro']
|
| 490 |
+
else:
|
| 491 |
+
stats['skip_critic'] = C['micro']
|
| 492 |
+
opt_critic.zero_grad(set_to_none=False)
|
| 493 |
+
return {k: v / C['micro'] for k, v in stats.items()}
|
| 494 |
+
|
| 495 |
+
@torch.no_grad()
|
| 496 |
+
def sample(tag):
|
| 497 |
+
nonlocal vae
|
| 498 |
+
if D.rank == 0:
|
| 499 |
+
if vae is None:
|
| 500 |
+
from diffusers import AutoencoderKLFlux2
|
| 501 |
+
vae = AutoencoderKLFlux2.from_pretrained(Path(C['model_dir']) / 'vae', torch_dtype=torch.float32,
|
| 502 |
+
local_files_only=True).to(dev).eval()
|
| 503 |
+
d = out / 'samples' / tag
|
| 504 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 505 |
+
images = []
|
| 506 |
+
for row in evals:
|
| 507 |
+
g = torch.Generator('cpu').manual_seed(C['eval_seed'])
|
| 508 |
+
z = torch.randn(1, model.tr.config.latent_channels, row['h'] // 16, row['w'] // 16, generator=g).to(dev)
|
| 509 |
+
x0 = ema.rollout(model, z, model.context(cache.get(row['id']).to(dev)), R)
|
| 510 |
+
images.append(decode(vae, x0))
|
| 511 |
+
from PIL import Image
|
| 512 |
+
Image.fromarray(images[-1]).save(d / f"{row['id']}.png")
|
| 513 |
+
contact_sheet(images, out / 'samples' / f'{tag}.jpg')
|
| 514 |
+
D.barrier()
|
| 515 |
+
|
| 516 |
+
def save(it):
|
| 517 |
+
if D.rank == 0:
|
| 518 |
+
state = {key: mod.state_dict() for key, mod in parts.items()}
|
| 519 |
+
state.update(it=it, gen_updates=gen_updates, config=C)
|
| 520 |
+
tmp = out / 'ckpt' / 'latest.tmp'
|
| 521 |
+
torch.save(state, tmp)
|
| 522 |
+
if latest.exists():
|
| 523 |
+
latest.replace(out / 'ckpt' / 'prev.pt')
|
| 524 |
+
tmp.replace(latest)
|
| 525 |
+
D.barrier()
|
| 526 |
+
|
| 527 |
+
if start == 0 and C['sample_every']:
|
| 528 |
+
sample('init')
|
| 529 |
+
t_last, log = time.time(), []
|
| 530 |
+
for it in range(start, C['iterations']):
|
| 531 |
+
entry = {'it': it}
|
| 532 |
+
if dmad:
|
| 533 |
+
gen_on = it >= C['critic_warmup']
|
| 534 |
+
entry.update(dmad_step(it, gen_on))
|
| 535 |
+
gen_updates += gen_on
|
| 536 |
+
elif idm:
|
| 537 |
+
entry['fake'] = sum(fake_step(it * C['fake_per_gen'] + k) for k in range(C['fake_per_gen'])) / C['fake_per_gen']
|
| 538 |
+
if it >= C['fake_warmup']:
|
| 539 |
+
entry.update(gen_step_idm(it))
|
| 540 |
+
gen_updates += 1
|
| 541 |
+
else:
|
| 542 |
+
entry['fake'] = sum(fake_step(it * C['fake_per_gen'] + k) for k in range(C['fake_per_gen'])) / C['fake_per_gen']
|
| 543 |
+
if it >= C['fake_warmup']:
|
| 544 |
+
entry.update(gen_step(it))
|
| 545 |
+
gen_updates += 1
|
| 546 |
+
log.append(entry)
|
| 547 |
+
done = it + 1
|
| 548 |
+
if done % C['log_every'] == 0:
|
| 549 |
+
keys = sorted({k for e in log for k in e if k != 'it'})
|
| 550 |
+
means = D.mean([sum(e.get(k, 0.0) for e in log) / len(log) for k in keys])
|
| 551 |
+
now = time.time()
|
| 552 |
+
if D.rank == 0:
|
| 553 |
+
rec = {'it': done, 'gen_updates': gen_updates, 's_per_it': round((now - t_last) / len(log), 2),
|
| 554 |
+
'mem_gib': round(torch.cuda.max_memory_allocated() / 2**30, 1),
|
| 555 |
+
**{k: round(v, 6) for k, v in zip(keys, means)}}
|
| 556 |
+
print(json.dumps(rec), flush=True)
|
| 557 |
+
with open(out / 'log.jsonl', 'a') as f:
|
| 558 |
+
f.write(json.dumps(rec) + '\n')
|
| 559 |
+
t_last, log = now, []
|
| 560 |
+
if C['sample_every'] and done % C['sample_every'] == 0:
|
| 561 |
+
sample(f'{done:06d}')
|
| 562 |
+
if C['save_every'] and done % C['save_every'] == 0:
|
| 563 |
+
save(done)
|
| 564 |
+
if C['export_every'] and done % C['export_every'] == 0 and D.rank == 0:
|
| 565 |
+
ema.export(out / 'export' / f'{done:06d}', {'iteration': str(done)})
|
| 566 |
+
save(C['iterations'])
|
| 567 |
+
if D.rank == 0:
|
| 568 |
+
ema.export(out / 'export' / 'final', {'iteration': str(C['iterations'])})
|
| 569 |
+
gen.export(out / 'export' / 'final-raw', {'iteration': str(C['iterations'])})
|
| 570 |
+
if C['sample_every']:
|
| 571 |
+
sample('final')
|
| 572 |
+
if D.on:
|
| 573 |
+
dist.destroy_process_group()
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
if __name__ == '__main__':
|
| 577 |
+
main()
|