akhaliq HF Staff commited on
Commit
bb64b09
·
verified ·
1 Parent(s): 81f19ad

IDM method for rapid_moss (data-space): DMD direction + L_pop moment matching, no adversarial

Browse files
Files changed (1) hide show
  1. 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()