krystv commited on
Commit
a80be80
·
verified ·
1 Parent(s): db9bd01

Upload lrf/train_v2.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. lrf/train_v2.py +390 -0
lrf/train_v2.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LRF v2 Training on CIFAR-10 with pre-trained TAESD VAE.
3
+
4
+ This script:
5
+ 1. Loads TAESD (pre-trained, frozen) as the image encoder/decoder
6
+ 2. Pre-computes all CIFAR-10 latents (fast, ~30s)
7
+ 3. Trains the RecursiveLatentCore denoiser on real latents
8
+ 4. Generates real images and saves them
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import time
14
+ import json
15
+ import torch
16
+ import torch.nn.functional as F
17
+ from torch.utils.data import DataLoader, TensorDataset
18
+ import torchvision
19
+ import torchvision.transforms as T
20
+ import numpy as np
21
+ from pathlib import Path
22
+
23
+ sys.path.insert(0, '/app')
24
+ from lrf.model_v2 import LRFv2, RectifiedFlowScheduler
25
+
26
+
27
+ def load_taesd(device='cpu'):
28
+ """Load pre-trained TAESD VAE."""
29
+ from diffusers import AutoencoderTiny
30
+ vae = AutoencoderTiny.from_pretrained('madebyollin/taesd', torch_dtype=torch.float32)
31
+ vae.eval()
32
+ vae.to(device)
33
+ for p in vae.parameters():
34
+ p.requires_grad_(False)
35
+ return vae
36
+
37
+
38
+ def precompute_latents(vae, dataset, batch_size=64, device='cpu'):
39
+ """Pre-compute all latent representations. Much faster than encoding on-the-fly."""
40
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=0)
41
+
42
+ all_latents = []
43
+ all_labels = []
44
+
45
+ total = len(loader)
46
+ print(f"Pre-computing latents for {len(dataset)} images ({total} batches)...", flush=True)
47
+ t0 = time.time()
48
+
49
+ with torch.no_grad():
50
+ for batch_idx, (images, labels) in enumerate(loader):
51
+ images = images.to(device)
52
+ latents = vae.encode(images).latents
53
+ all_latents.append(latents.cpu())
54
+ all_labels.append(labels)
55
+ if (batch_idx + 1) % 50 == 0 or batch_idx == 0:
56
+ elapsed = time.time() - t0
57
+ print(f" Batch {batch_idx+1}/{total} ({elapsed:.0f}s)", flush=True)
58
+
59
+ all_latents = torch.cat(all_latents, dim=0)
60
+ all_labels = torch.cat(all_labels, dim=0)
61
+
62
+ dt = time.time() - t0
63
+ print(f"Done in {dt:.1f}s. Latent shape: {all_latents.shape}", flush=True)
64
+ print(f"Latent stats: mean={all_latents.mean():.4f}, std={all_latents.std():.4f}, "
65
+ f"min={all_latents.min():.4f}, max={all_latents.max():.4f}", flush=True)
66
+
67
+ return all_latents, all_labels
68
+
69
+
70
+ def train_denoiser(
71
+ config=None,
72
+ num_epochs=50,
73
+ batch_size=128,
74
+ lr=2e-4,
75
+ device='cpu',
76
+ output_dir='/app/lrf_v2_output',
77
+ save_every=10,
78
+ ):
79
+ """Train the LRF denoiser on CIFAR-10 latents."""
80
+ os.makedirs(output_dir, exist_ok=True)
81
+
82
+ print("=" * 60)
83
+ print("LRF v2 - Training on CIFAR-10")
84
+ print("=" * 60)
85
+
86
+ # 1. Load TAESD
87
+ print("\n[Step 1] Loading TAESD VAE...")
88
+ vae = load_taesd(device)
89
+ print(f" TAESD loaded: {sum(p.numel() for p in vae.parameters()):,} params (frozen)")
90
+
91
+ # 2. Load CIFAR-10
92
+ print("\n[Step 2] Loading CIFAR-10...")
93
+ transform = T.Compose([
94
+ T.ToTensor(),
95
+ T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), # -> [-1, 1]
96
+ ])
97
+
98
+ # CIFAR-10 is 32x32, TAESD expects multiples of 8 -> resize to 32
99
+ # Actually TAESD works on any size, 32x32 -> 4x4 latent (f=8)
100
+ trainset = torchvision.datasets.CIFAR10(
101
+ root='/app/data', train=True, download=True, transform=transform,
102
+ )
103
+ testset = torchvision.datasets.CIFAR10(
104
+ root='/app/data', train=False, download=True, transform=transform,
105
+ )
106
+ print(f" Train: {len(trainset)}, Test: {len(testset)}")
107
+ print(f" Image size: {trainset[0][0].shape}")
108
+
109
+ # 3. Pre-compute latents (or load from cache)
110
+ print("\n[Step 3] Pre-computing latents...", flush=True)
111
+ cache_path = os.path.join(output_dir, 'latent_cache.pt')
112
+ if os.path.exists(cache_path):
113
+ print(" Loading cached latents...", flush=True)
114
+ cache = torch.load(cache_path, weights_only=True)
115
+ train_latents = cache['train_latents']
116
+ train_labels = cache['train_labels']
117
+ test_latents = cache['test_latents']
118
+ test_labels = cache['test_labels']
119
+ print(f" Loaded from cache. Train: {train_latents.shape}, Test: {test_latents.shape}", flush=True)
120
+ else:
121
+ train_latents, train_labels = precompute_latents(vae, trainset, batch_size=256, device=device)
122
+ test_latents, test_labels = precompute_latents(vae, testset, batch_size=256, device=device)
123
+ torch.save({
124
+ 'train_latents': train_latents, 'train_labels': train_labels,
125
+ 'test_latents': test_latents, 'test_labels': test_labels,
126
+ }, cache_path)
127
+ print(f" Cached latents to {cache_path}", flush=True)
128
+
129
+ # Verify VAE reconstruction works
130
+ print("\n[Step 3b] Verifying VAE reconstruction...")
131
+ with torch.no_grad():
132
+ sample_imgs = torch.stack([trainset[i][0] for i in range(8)]).to(device)
133
+ sample_lats = vae.encode(sample_imgs).latents
134
+ sample_recs = vae.decode(sample_lats).sample
135
+ recon_mse = F.mse_loss(sample_recs, sample_imgs).item()
136
+ print(f" VAE reconstruction MSE on real images: {recon_mse:.4f}")
137
+
138
+ # Save reconstruction grid
139
+ save_image_grid(
140
+ torch.cat([sample_imgs[:4], sample_recs[:4]], dim=0),
141
+ os.path.join(output_dir, 'vae_reconstruction.png'),
142
+ nrow=4, title='Top: Original, Bottom: TAESD Reconstruction'
143
+ )
144
+ print(f" Saved VAE reconstruction grid to {output_dir}/vae_reconstruction.png")
145
+
146
+ # Normalize latents for better training
147
+ lat_mean = train_latents.mean()
148
+ lat_std = train_latents.std()
149
+ print(f"\n Latent mean: {lat_mean:.4f}, std: {lat_std:.4f}")
150
+ # Scale latents to roughly unit variance
151
+ latent_scale = lat_std.item()
152
+
153
+ # Create dataset of (latent, label)
154
+ train_ds = TensorDataset(train_latents, train_labels)
155
+ train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True,
156
+ num_workers=0, drop_last=True)
157
+
158
+ # 4. Create model
159
+ print("\n[Step 4] Creating LRF denoiser...")
160
+ config = config or LRFv2.small_config()
161
+ config['latent_ch'] = train_latents.shape[1] # Should be 4
162
+ model = LRFv2(config).to(device)
163
+ params = model.count_params()
164
+ print(f" Config: dim={config['dim']}, blocks={config['num_blocks']}, "
165
+ f"T_inner={config['T_inner']}, T_outer={config['T_outer']}")
166
+ print(f" Parameters: {params['total']:,} total, {params['core']:,} core")
167
+ print(f" Effective depth: {config['T_outer'] * config['T_inner'] * config['num_blocks']} layers "
168
+ f"from {config['num_blocks']} blocks")
169
+
170
+ # 5. Training
171
+ print(f"\n[Step 5] Training for {num_epochs} epochs...")
172
+ scheduler = RectifiedFlowScheduler()
173
+ optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01, betas=(0.9, 0.95))
174
+
175
+ # Cosine annealing
176
+ lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
177
+ optimizer, T_max=num_epochs * len(train_loader), eta_min=lr * 0.01
178
+ )
179
+
180
+ # EMA for stable sampling
181
+ ema_decay = 0.999
182
+ ema_params = {name: p.clone().detach() for name, p in model.named_parameters()}
183
+
184
+ loss_history = []
185
+ best_loss = float('inf')
186
+
187
+ for epoch in range(num_epochs):
188
+ model.train()
189
+ epoch_loss = 0.0
190
+ num_batches = 0
191
+
192
+ for latents, labels in train_loader:
193
+ latents = latents.to(device)
194
+ labels = labels.to(device)
195
+ B = latents.shape[0]
196
+
197
+ # Sample timesteps and noise
198
+ t = scheduler.sample_timesteps(B, device)
199
+ noise = torch.randn_like(latents)
200
+
201
+ # Create noisy latent
202
+ z_t = scheduler.add_noise(latents, noise, t)
203
+
204
+ # Predict velocity (with 10% CFG dropout)
205
+ v_pred = model.predict_velocity(z_t, t, labels, cfg_dropout=0.1)
206
+
207
+ # Velocity target
208
+ v_target = scheduler.get_velocity_target(latents, noise)
209
+
210
+ # MSE loss with min-SNR weighting
211
+ loss_per_sample = (v_pred - v_target).pow(2).mean(dim=[1, 2, 3])
212
+
213
+ # SNR weighting: upweight middle timesteps
214
+ w = 1.0 / (t * (1 - t) + 0.01)
215
+ w = w / w.mean()
216
+ loss = (loss_per_sample * w).mean()
217
+
218
+ optimizer.zero_grad()
219
+ loss.backward()
220
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
221
+ optimizer.step()
222
+ lr_scheduler.step()
223
+
224
+ # EMA update
225
+ with torch.no_grad():
226
+ for name, p in model.named_parameters():
227
+ ema_params[name].mul_(ema_decay).add_(p, alpha=1 - ema_decay)
228
+
229
+ epoch_loss += loss.item()
230
+ num_batches += 1
231
+
232
+ avg_loss = epoch_loss / num_batches
233
+ loss_history.append(avg_loss)
234
+
235
+ if avg_loss < best_loss:
236
+ best_loss = avg_loss
237
+
238
+ if (epoch + 1) % 5 == 0 or epoch == 0:
239
+ current_lr = optimizer.param_groups[0]['lr']
240
+ print(f" Epoch {epoch+1:3d}/{num_epochs}: loss={avg_loss:.4f}, "
241
+ f"best={best_loss:.4f}, lr={current_lr:.2e}", flush=True)
242
+
243
+ # Save and generate samples periodically
244
+ if (epoch + 1) % save_every == 0 or epoch == num_epochs - 1:
245
+ # Swap to EMA for sampling
246
+ saved_params = {}
247
+ with torch.no_grad():
248
+ for name, p in model.named_parameters():
249
+ saved_params[name] = p.clone()
250
+ p.copy_(ema_params[name])
251
+
252
+ # Generate samples
253
+ model.eval()
254
+ samples = generate_samples(model, vae, scheduler, device,
255
+ num_samples=16, num_steps=10, cfg_scale=2.0)
256
+
257
+ save_image_grid(
258
+ samples,
259
+ os.path.join(output_dir, f'samples_epoch{epoch+1:03d}.png'),
260
+ nrow=8, title=f'Epoch {epoch+1}, Loss={avg_loss:.4f}'
261
+ )
262
+
263
+ # Restore original params
264
+ with torch.no_grad():
265
+ for name, p in model.named_parameters():
266
+ p.copy_(saved_params[name])
267
+
268
+ # Save checkpoint
269
+ torch.save({
270
+ 'model_state': model.state_dict(),
271
+ 'ema_params': ema_params,
272
+ 'config': config,
273
+ 'epoch': epoch + 1,
274
+ 'loss': avg_loss,
275
+ 'latent_scale': latent_scale,
276
+ 'loss_history': loss_history,
277
+ }, os.path.join(output_dir, 'checkpoint.pt'))
278
+
279
+ # Final generation with EMA
280
+ with torch.no_grad():
281
+ for name, p in model.named_parameters():
282
+ p.copy_(ema_params[name])
283
+
284
+ model.eval()
285
+
286
+ # Generate class-conditional samples
287
+ print("\n[Step 6] Generating final samples...")
288
+ cifar_classes = ['airplane', 'automobile', 'bird', 'cat', 'deer',
289
+ 'dog', 'frog', 'horse', 'ship', 'truck']
290
+
291
+ all_samples = []
292
+ for cls_idx in range(10):
293
+ samples = generate_samples(model, vae, scheduler, device,
294
+ num_samples=4, num_steps=50, cfg_scale=3.0,
295
+ class_label=cls_idx)
296
+ all_samples.append(samples)
297
+
298
+ all_samples = torch.cat(all_samples, dim=0)
299
+ save_image_grid(
300
+ all_samples,
301
+ os.path.join(output_dir, 'final_class_conditional.png'),
302
+ nrow=4, title='Class-conditional generation (rows: airplane, auto, bird, cat, deer, dog, frog, horse, ship, truck)'
303
+ )
304
+
305
+ # Save loss plot
306
+ save_loss_plot(loss_history, os.path.join(output_dir, 'loss.png'))
307
+
308
+ # Save config
309
+ with open(os.path.join(output_dir, 'config.json'), 'w') as f:
310
+ json.dump(config, f, indent=2)
311
+
312
+ print(f"\n{'='*60}")
313
+ print(f"Training complete! Best loss: {best_loss:.4f}")
314
+ print(f"Output directory: {output_dir}")
315
+ print(f"{'='*60}")
316
+
317
+ return model, vae, loss_history
318
+
319
+
320
+ def generate_samples(model, vae, scheduler, device, num_samples=8,
321
+ num_steps=20, cfg_scale=2.0, class_label=None):
322
+ """Generate images from the model."""
323
+ model.eval()
324
+
325
+ # Latent shape for CIFAR-10: [B, 4, 4, 4] (32x32 image, f=8)
326
+ shape = (num_samples, 4, 4, 4)
327
+
328
+ if class_label is not None:
329
+ labels = torch.full((num_samples,), class_label, dtype=torch.long, device=device)
330
+ else:
331
+ labels = torch.randint(0, 10, (num_samples,), device=device)
332
+
333
+ z = scheduler.sample(model, shape, labels, num_steps=num_steps,
334
+ cfg_scale=cfg_scale, device=device)
335
+
336
+ # Decode through TAESD
337
+ with torch.no_grad():
338
+ images = vae.decode(z.to(device)).sample
339
+
340
+ return images.clamp(-1, 1).cpu()
341
+
342
+
343
+ def save_image_grid(images, path, nrow=8, title=''):
344
+ """Save image grid using torchvision."""
345
+ # Convert from [-1,1] to [0,1]
346
+ images = (images + 1) / 2
347
+ images = images.clamp(0, 1)
348
+
349
+ grid = torchvision.utils.make_grid(images, nrow=nrow, padding=2, normalize=False)
350
+
351
+ # Save using PIL
352
+ from PIL import Image
353
+ grid_np = grid.permute(1, 2, 0).numpy()
354
+ grid_np = (grid_np * 255).astype(np.uint8)
355
+ img = Image.fromarray(grid_np)
356
+ img.save(path)
357
+
358
+
359
+ def save_loss_plot(losses, path):
360
+ """Save loss curve."""
361
+ try:
362
+ import matplotlib
363
+ matplotlib.use('Agg')
364
+ import matplotlib.pyplot as plt
365
+
366
+ plt.figure(figsize=(10, 4))
367
+ plt.plot(losses)
368
+ plt.xlabel('Epoch')
369
+ plt.ylabel('Loss')
370
+ plt.title('Training Loss')
371
+ plt.grid(True, alpha=0.3)
372
+ plt.savefig(path, dpi=100, bbox_inches='tight')
373
+ plt.close()
374
+ except ImportError:
375
+ print("matplotlib not available, skipping loss plot")
376
+
377
+
378
+ if __name__ == '__main__':
379
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
380
+ print(f"Device: {device}")
381
+
382
+ train_denoiser(
383
+ config=LRFv2.fast_config(),
384
+ num_epochs=30,
385
+ batch_size=64,
386
+ lr=3e-4,
387
+ device=device,
388
+ output_dir='/app/lrf_v2_output',
389
+ save_every=5,
390
+ )