Text-to-Image
Diffusers
Safetensors
flow-matching
pyramidal-unet
256x256
ettin-150m
from-scratch
ra
udit
Instructions to use AiArtLab/Ra with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use AiArtLab/Ra with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("AiArtLab/Ra", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
Download src/train.py from AiArtLab/Ra: direct link, hf CLI and curl.
- Browser
- Download file 29.3 kB
-
https://huggingface.co/AiArtLab/Ra/resolve/main/src/train.py
- Command line
-
hf download hf://AiArtLab/Ra/src/train.py
-
curl -L -o train.py https://huggingface.co/AiArtLab/Ra/resolve/main/src/train.py
29.3 kB
| #!/usr/bin/env python3 | |
| """Обучение UDiT на папке «картинка + .txt». Квадратные кадры (--size), кэш латентов и текста. | |
| python3 src/train.py --data ~/dataset/butterflies_256sq/train \ | |
| --eval ~/dataset/butterflies_256sq/eval --epochs 80 --batch 8 | |
| Что внутри (перенесено из привычек agat/train.py): | |
| * бюджет в эпохах (`--epochs`) или шагах (`--steps`), lr: прогрев → cosine → полка; | |
| * AdamW8bit (bitsandbytes), если он есть, иначе обычный AdamW — печатается, что взято; | |
| * дропаут подписи (`--drop-text`) для CFG и дропаут слов (`--drop-word`) внутри подписи; | |
| * сэмплы на старте (шаг 0), каждые `--sample-every`; держим `--keep` последних чекпойнтов и | |
| `--samples-keep` сеток; сохранение атомарное; | |
| * `--probe`: подбирает максимальный батч, который влезает в память, и печатает его; | |
| * EMA сглаживание печатаемого лосса (`--ema-decay`), `--seed` случайный по умолчанию (печатается); | |
| * метрики: mse_lat (общий, low t<0.3, high t>0.7), mse_pix, switch, tail(z0), hf(gen)/hf(AE), skip; | |
| * SIGUSR1 печатает стеки всех потоков (видно, ждёт процесс GPU или сеть, без py-spy). | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import faulthandler | |
| import json | |
| import math | |
| import os | |
| import random | |
| import shutil | |
| import signal | |
| import time | |
| from pathlib import Path | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| signal.signal(signal.SIGINT, signal.default_int_handler) | |
| faulthandler.register(signal.SIGUSR1) | |
| import torch # noqa: E402 | |
| import torch.nn.functional as F # noqa: E402 | |
| from model import DEV, UDiT # noqa: E402 | |
| from text import EttinText # noqa: E402 | |
| from ae.model import AutoEncoder, patchify # noqa: E402 | |
| ROOT = Path(__file__).resolve().parent.parent | |
| # ------------------------------------------------------------------ расписание | |
| def lr_at(step: int, base: float, min_lr: float, warmup: int, period: int, start: int = 0) -> float: | |
| """Прогрев `warmup` шагов от `start`, затем cosine до `min_lr` за `period` шагов.""" | |
| rel = step - start | |
| if warmup and rel <= warmup: | |
| return base * min(1.0, rel / max(warmup, 1)) | |
| if period <= warmup: | |
| return base | |
| p = min(1.0, (rel - warmup) / (period - warmup)) | |
| return min_lr + (base - min_lr) * 0.5 * (1 + math.cos(math.pi * p)) | |
| class HFImageStream: | |
| """Поток parquet-шардов HF: декод/ресайз/кроп в фоновых потоках, обучение не ждёт диск. | |
| Латент и текст НЕ кэшируются (каждая картинка встречается один раз), поэтому шаг дороже, | |
| чем на папке: +3-4 мс на картинку у AE и +1.2 мс у Ettin. | |
| """ | |
| def __init__(self, dataset, parts, size, fields, lo, hi, shuffle_buffer=1000, prefetch=48, | |
| workers=8, seed=0): | |
| import queue | |
| self.q, self.stop = queue.Queue(maxsize=prefetch), False | |
| self.size, self.fields, self.lo, self.hi = size, fields, lo, hi | |
| self.dataset, self.parts = dataset, parts | |
| self.buffer, self.workers, self.seed = shuffle_buffer, workers, seed | |
| import threading | |
| threading.Thread(target=self._loop, daemon=True).start() | |
| def _items(self): | |
| """Шарды всех частей перемешиваются между собой (иначе эпоха идёт блоками по частям).""" | |
| import random | |
| from datasets import load_dataset | |
| from huggingface_hub import list_repo_files | |
| repo = list_repo_files(self.dataset, repo_type="dataset") | |
| files = [f for f in repo | |
| if f.endswith(".parquet") and any(f.startswith(p + "/") for p in self.parts)] | |
| if not files: # запасной путь, если листинг не отдал файлы | |
| files = [f"hf://datasets/{self.dataset}/{p}/*.parquet" for p in self.parts] | |
| else: | |
| random.Random(self.seed).shuffle(files) | |
| files = [f"hf://datasets/{self.dataset}/{f}" for f in files] | |
| print(f"[stream] шардов: {len(files)} из частей {self.parts}", flush=True) | |
| ds = load_dataset("parquet", data_files=files, split="train", streaming=True) | |
| return ds.shuffle(buffer_size=self.buffer, seed=self.seed) | |
| def _prepare(self, item): | |
| from PIL import Image | |
| import numpy as np | |
| im = item["image"] | |
| if im.mode != "RGB": | |
| im = im.convert("RGB") | |
| w, h = im.size | |
| sc = self.size / min(w, h) | |
| if abs(sc - 1.0) > 1e-3: | |
| im = im.resize((max(self.size, round(w * sc)), max(self.size, round(h * sc))), Image.LANCZOS) | |
| w, h = im.size | |
| left, top = (w - self.size) // 2, (h - self.size) // 2 | |
| im = im.crop((left, top, left + self.size, top + self.size)) | |
| arr = torch.from_numpy(np.asarray(im, dtype=np.float32)).permute(2, 0, 1) / 127.5 - 1.0 | |
| return arr, self._caption(item) | |
| def _caption(self, item): | |
| clean = lambda x: (x or "").replace("\n", " ").strip() # noqa: E731 | |
| for f in self.fields: | |
| t = clean(item.get(f)) | |
| if self.lo <= len(t) <= self.hi: | |
| return t | |
| return clean(item.get("caption_detail")) | |
| def _loop(self): | |
| import collections | |
| from concurrent.futures import ThreadPoolExecutor | |
| with ThreadPoolExecutor(max_workers=self.workers) as pool: | |
| while not self.stop: | |
| try: | |
| it = iter(self._items()) | |
| flying = collections.deque() | |
| while not self.stop: | |
| while len(flying) < self.workers: | |
| flying.append(pool.submit(self._prepare, next(it))) | |
| self.q.put(flying.popleft().result()) | |
| except Exception as e: | |
| print(f"[stream] сбой ({type(e).__name__}: {e}), переподключение через 5 с", flush=True) | |
| time.sleep(5) | |
| def batch(self, n): | |
| imgs, caps = [], [] | |
| while len(imgs) < n: | |
| arr, cap = self.q.get() | |
| imgs.append(arr); caps.append(cap) | |
| return torch.stack(imgs), caps | |
| def hf_energy(t: torch.Tensor) -> float: | |
| k = torch.tensor([[0.0, 1.0, 0.0], [1.0, -4.0, 1.0], [0.0, 1.0, 0.0]], device=t.device) | |
| return F.conv2d(t.float().mean(1, keepdim=True), k.view(1, 1, 3, 3), padding=1).std().item() | |
| def psnr(a, b) -> float: | |
| return 10 * math.log10(4 / max((a.clamp(-1, 1) - b).pow(2).mean().item(), 1e-12)) | |
| def rotate(path: Path, keep: int, pattern: str): | |
| """Оставить `keep` последних файлов, совпадающих с pattern (по номеру шага в имени).""" | |
| files = sorted(path.glob(pattern)) | |
| for f in files[:-keep]: | |
| f.unlink() | |
| def main(): | |
| ap = argparse.ArgumentParser(description="UDiT: обучение на папке пар <имя>.jpg + <имя>.txt") | |
| ap.add_argument("--data", default=str(Path.home() / "dataset" / "butterflies_256sq" / "train")) | |
| ap.add_argument("--eval", default=str(Path.home() / "dataset" / "butterflies_256sq" / "eval")) | |
| ap.add_argument("--limit", type=int, default=0, help="взять только первые N картинок (папка)") | |
| ap.add_argument("--hf-dataset", default="", help="например LucasFang/FLUX-Reason-6M (поток)") | |
| ap.add_argument("--parts", default="Aesthetics-Part01,Aesthetics-Part02,Imaginative,Text") | |
| ap.add_argument("--caption-fields", default="caption_entity,caption_composition", | |
| help="короткие поля подписи по порядку предпочтения (иначе caption_detail)") | |
| ap.add_argument("--caption-len", default=(40, 400), nargs=2, type=int, metavar=("MIN", "MAX")) | |
| ap.add_argument("--shuffle-buffer", type=int, default=1000) | |
| ap.add_argument("--prefetch", type=int, default=48) | |
| ap.add_argument("--workers", type=int, default=8) | |
| ap.add_argument("--images-per-epoch", type=int, default=5_890_279, | |
| help="сколько картинок в эпохе потока (для пересчёта --epochs в шаги)") | |
| ap.add_argument("--size", type=int, default=256, help="разрешение обучения (кадры квадратные)") | |
| ap.add_argument("--text-len", type=int, default=192, help="предел длины подписи в токенах") | |
| ap.add_argument("--epochs", type=float, default=None, help="бюджет в эпохах (приоритетнее --steps)") | |
| ap.add_argument("--steps", type=int, default=20000, help="бюджет в шагах оптимизатора") | |
| ap.add_argument("--batch", type=int, default=8) | |
| ap.add_argument("--accum", type=int, default=1, help="накопление: эффективный батч = batch*accum") | |
| ap.add_argument("--probe", action="store_true", help="подобрать максимальный батч под память и выйти") | |
| ap.add_argument("--lr", type=float, default=1e-4) | |
| ap.add_argument("--lr-warmup", type=int, default=500) | |
| ap.add_argument("--lr-min", type=float, default=None, help="нижняя точка cosine (по умолчанию 10%% от --lr)") | |
| ap.add_argument("--lr-period", type=int, default=0, help="к какому шагу cosine доходит до --lr-min") | |
| ap.add_argument("--drop-text", type=float, default=0.1, help="доля строк с пустой подписью (CFG)") | |
| ap.add_argument("--drop-word", type=float, default=0.05, help="доля токенов подписи, скрытых в строке") | |
| ap.add_argument("--cfg", type=float, default=1.5, help="CFG при показе сэмплов") | |
| ap.add_argument("--sample-steps", type=int, default=8, help="шагов тела при сэмплинге") | |
| ap.add_argument("--tail-steps", type=int, default=4, help="шагов декодера AE") | |
| ap.add_argument("--lr-tail", type=float, default=3e-5) | |
| ap.add_argument("--tail", choices=["frozen", "train"], default="train") | |
| ap.add_argument("--init", default=None, help="стартовые веса: чекпойнт или папка transformer/") | |
| ap.add_argument("--resume", action="store_true", help="продолжить с <out>/last.pt") | |
| ap.add_argument("--out", default=str(ROOT / "tmp" / "run")) | |
| ap.add_argument("--log-every", type=int, default=50) | |
| ap.add_argument("--sample-every", type=int, default=1000, help="0 = не сэмплировать") | |
| ap.add_argument("--save-every", type=int, default=2000, help="0 = только в конце") | |
| ap.add_argument("--keep", type=int, default=2, help="сколько чекпойнтов держать") | |
| ap.add_argument("--samples-keep", type=int, default=4, help="сколько сеток сэмплов держать") | |
| ap.add_argument("--ema-decay", type=float, default=0.995, help="сглаживание печатаемого лосса") | |
| ap.add_argument("--seed", type=int, default=None) | |
| args = ap.parse_args() | |
| seed = args.seed if args.seed is not None else random.SystemRandom().randrange(2 ** 31) | |
| torch.manual_seed(seed) | |
| print(f"[seed] {seed}", flush=True) | |
| out = Path(args.out); (out / "samples").mkdir(parents=True, exist_ok=True) | |
| (out / "ckpt").mkdir(exist_ok=True) | |
| log_path = out / "log.csv" | |
| if not (args.resume and log_path.exists()): | |
| log_path.write_text("step,mse_lat,low,high,mse_pix,switch,bright,contrast,lat_pc_min,hf_gen,tail_z0,sec\n") | |
| from PIL import Image | |
| import numpy as np | |
| def load(path: Path): | |
| im = Image.open(path).convert("RGB") | |
| return torch.from_numpy(np.asarray(im, dtype=np.float32)).permute(2, 0, 1) / 127.5 - 1 | |
| # ---------------------------------------------------------------- данные | |
| stream = None | |
| if args.hf_dataset: | |
| stream = HFImageStream(args.hf_dataset, args.parts.split(","), args.size, | |
| args.caption_fields.split(","), args.caption_len[0], args.caption_len[1], | |
| args.shuffle_buffer, args.prefetch, args.workers, seed) | |
| n = args.images_per_epoch | |
| per_epoch = max(1, n // (args.batch * args.accum)) | |
| steps = int(round(args.epochs * per_epoch)) if args.epochs else args.steps | |
| print(f"[data] поток {args.hf_dataset} [{args.parts}], ~{n} картинок/эпоха, " | |
| f"{per_epoch} шагов/эпоха → бюджет {steps} шагов " | |
| f"(~{steps * args.batch / 62 / 3600:.1f} ч при 62 картинках/с)", flush=True) | |
| ae = AutoEncoder.from_pretrained(str(ROOT / "vae"), device=DEV) | |
| enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len) | |
| HS = MK = X = Z = None | |
| HS_E, MK_E = enc.slices([""], pad_to_len=args.text_len) | |
| HS_E = HS_E.to(torch.bfloat16) | |
| else: | |
| files = sorted(Path(args.data).glob("*.jpg")) + sorted(Path(args.data).glob("*.png")) | |
| if args.limit: | |
| files = files[: args.limit] | |
| caps = [f.with_suffix(".txt").read_text().strip() for f in files] | |
| ae = AutoEncoder.from_pretrained(str(ROOT / "vae"), device=DEV) | |
| enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len) | |
| X = torch.stack([load(f) for f in files]).to(DEV) | |
| with torch.no_grad(): | |
| Z = torch.cat([ae.encode(X[i:i + 8]) for i in range(0, len(X), 8)]) | |
| HS, MK = enc.slices(caps) | |
| HS, MK = HS.to(torch.bfloat16), MK | |
| HS_E, MK_E = enc.slices([""], pad_to_len=HS.shape[1]) | |
| HS_E = HS_E.to(torch.bfloat16) | |
| n = len(files) | |
| per_epoch = max(1, n // (args.batch * args.accum)) | |
| steps = int(round(args.epochs * per_epoch)) if args.epochs else args.steps | |
| print(f"[data] {n} пар, латент {tuple(Z.shape[1:])}, текст {tuple(HS.shape[1:])}, " | |
| f"эпоха = {per_epoch} шагов → бюджет {steps}", flush=True) | |
| # ---------------------------------------------------------------- модель и оптимизатор | |
| m = UDiT(ae, text_dim=enc.dim).to(DEV) | |
| start = 0 | |
| def load_body(sd): | |
| own = m.body.state_dict() | |
| take = {k: v for k, v in sd.items() if k in own and own[k].shape == v.shape} | |
| m.body.load_state_dict(take, strict=False) | |
| return len(take) | |
| if args.init: | |
| p = Path(args.init) | |
| if p.is_dir(): | |
| from export import load_exported | |
| cfg = load_exported(m, enc, ae, p, DEV) | |
| print(f"[init] папка {p}, шаг {cfg.get('step')}", flush=True) | |
| else: | |
| sd = torch.load(p, map_location=DEV) | |
| n_take = load_body(sd["body"]); enc.mixer.load_state_dict(sd["mixer"]) | |
| if sd.get("decoder") is not None and args.tail == "train": | |
| ae.decoder.load_state_dict(sd["decoder"]) | |
| start = int(sd.get("step", 0)) | |
| print(f"[init] {p}: перенесено {n_take} тензоров, шаг {start}", flush=True) | |
| if args.resume and (out / "last.pt").exists(): | |
| sd = torch.load(out / "last.pt", map_location=DEV) | |
| load_body(sd["body"]); enc.mixer.load_state_dict(sd["mixer"]) | |
| if sd.get("decoder") is not None and args.tail == "train": | |
| ae.decoder.load_state_dict(sd["decoder"]) | |
| start = int(sd.get("step", 0)) | |
| print(f"[resume] {out / 'last.pt'} → шаг {start}", flush=True) | |
| n_body = sum(p.numel() for p in m.body.parameters()) | |
| n_tail = sum(p.numel() for p in ae.decoder.parameters()) | |
| print(f"[model] тело {n_body/1e6:.1f}M + миксер {sum(p.numel() for p in enc.trainable())/1e6:.2f}M " | |
| f"+ хвост {n_tail/1e6:.1f}M [{args.tail}]", flush=True) | |
| groups = [{"params": list(m.body.parameters()), "lr": args.lr}, | |
| {"params": enc.trainable(), "lr": args.lr}] | |
| if args.tail == "train": | |
| ae.decoder.requires_grad_(True) | |
| groups.append({"params": list(ae.decoder.parameters()), "lr": args.lr_tail}) | |
| params = [p for g in groups for p in g["params"]] | |
| try: | |
| import bitsandbytes as bnb | |
| opt = bnb.optim.AdamW8bit(groups, betas=(0.9, 0.95), weight_decay=0.0) | |
| print("[opt] AdamW8bit", flush=True) | |
| except Exception as e: | |
| opt = torch.optim.AdamW(groups, betas=(0.9, 0.95), weight_decay=0.0) | |
| print(f"[opt] AdamW fp32 (8bit недоступен: {type(e).__name__})", flush=True) | |
| lr_min = args.lr_min if args.lr_min is not None else args.lr * 0.1 | |
| period = args.lr_period or steps # длина cosine в шагах, отсчёт от start | |
| print(f"[lr] {args.lr:g}: прогрев {args.lr_warmup} от шага {start}, cosine до {lr_min:g} к шагу {start + period}", flush=True) | |
| # ---------------------------------------------------------------- показ сэмплов | |
| SAMPLE_PROMPTS = [ | |
| "A photograph of a red fox sleeping on a mossy log in a misty forest at dawn.", | |
| "A city made of glass where rivers of light flow between the towers, cinematic lighting.", | |
| 'A neon sign reading "hello world" mounted on a brick wall at night in the rain.', | |
| "An impressionist oil painting of a woman with a parasol standing by a lake.", | |
| ] | |
| show_txt, show_x = [], [] | |
| if stream is not None: | |
| for cap in SAMPLE_PROMPTS: | |
| hs, mk = enc.slices([cap]) | |
| show_txt.append((enc.mix(hs.float()), mk)) | |
| else: | |
| ev_all = sorted(Path(args.eval).glob("*.jpg")) + sorted(Path(args.eval).glob("*.png")) | |
| ev_cls = {} | |
| for f in ev_all: | |
| ev_cls.setdefault(f.stem.split("_")[0], f) | |
| show = [ev_cls[k] for k in list(ev_cls)[:4]] or ev_all[:4] | |
| for f in show: | |
| hs, mk = enc.slices([f.with_suffix(".txt").read_text().strip()], pad_to_len=HS.shape[1]) | |
| show_txt.append((enc.mix(hs.float()), mk)) | |
| show_x.append(load(f)[None].to(DEV)) | |
| def sample_grid(step): | |
| tiles, hfs, ys = [], [], [] | |
| for txt, mk in show_txt: | |
| y = m.sample(txt, mk, lat_hw[0], lat_hw[1], steps=args.sample_steps, | |
| tail_steps=args.tail_steps, cfg=args.cfg, | |
| null_text=enc.mix(HS_E.float()), null_mask=MK_E, | |
| generator=torch.Generator(DEV).manual_seed(0)) | |
| tiles.append(Image.fromarray(((y[0].clamp(-1, 1) + 1) * 127.5).byte() | |
| .permute(1, 2, 0).cpu().numpy())) | |
| hfs.append(hf_energy(y)); ys.append(y) | |
| if show_x: | |
| ae_rec = ae.decode_raw(ae.encode(show_x[0]) * ae.latent_std + ae.latent_mean, | |
| steps=args.tail_steps, generator=torch.Generator(DEV).manual_seed(0)) | |
| hf_ae = hf_energy(ae_rec) | |
| else: | |
| hf_ae = float("nan") | |
| W, H = tiles[0].size | |
| canvas = Image.new("RGB", (W * len(tiles), H), "white") | |
| for i, t in enumerate(tiles): | |
| canvas.paste(t, (i * W, 0)) | |
| canvas.save(out / "samples" / "latest.png") | |
| canvas.save(out / "samples" / f"step_{step:07d}.png") | |
| rotate(out / "samples", args.samples_keep, "step_*.png") | |
| bright = float(torch.stack(ys).mean()) | |
| contrast = float(torch.stack(ys).std()) | |
| return hfs[0], hf_ae, canvas, bright, contrast | |
| def save(step): | |
| sd = {"body": {k: v.cpu() for k, v in m.body.state_dict().items()}, | |
| "mixer": {k: v.cpu() for k, v in enc.mixer.state_dict().items()}, | |
| "decoder": ({k: v.cpu() for k, v in ae.decoder.state_dict().items()} | |
| if args.tail == "train" else None), | |
| "step": step, "args": vars(args), "seed": seed} | |
| torch.save(sd, out / "last.pt") | |
| torch.save(sd, out / "ckpt" / f"step_{step:07d}.pt") | |
| rotate(out / "ckpt", args.keep, "step_*.pt") | |
| # ---------------------------------------------------------------- цикл | |
| lat_hw = (args.size // 8, args.size // 8) | |
| pkx = m.ae._packing([lat_hw] * args.batch) | |
| if args.probe: | |
| best = args.batch | |
| for bs in (args.batch, args.batch * 2, args.batch * 4, args.batch * 8): | |
| try: | |
| z = torch.randn(bs, 32, lat_hw[0], lat_hw[1], device=DEV) | |
| t = torch.rand(bs, device=DEV) | |
| if stream is not None: # в потоке текста нет — берём фиктивный | |
| hs_probe, mk_probe = enc.slices(["probe caption"] * bs) | |
| else: | |
| hs_probe, mk_probe = HS[:bs], MK[:bs] | |
| with torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"): | |
| m.body(z, t, enc.mix(hs_probe.float()), mk_probe).float().pow(2).mean().backward() | |
| best = bs | |
| m.zero_grad(set_to_none=True) | |
| except torch.OutOfMemoryError: | |
| m.zero_grad(set_to_none=True); torch.cuda.empty_cache(); break | |
| print(f"[probe] максимальный батч ≈ {best} (пик {torch.cuda.max_memory_allocated()/1e9:.1f} ГБ)", | |
| flush=True) | |
| if stream is not None: | |
| stream.stop = True | |
| import os as _os | |
| _os._exit(0) | |
| if stream is not None: | |
| def get_batch(): | |
| x, caps = stream.batch(args.batch) | |
| x = x.to(DEV) | |
| with torch.no_grad(): | |
| z = ae.encode(x) | |
| hs, mk = enc.slices(caps) | |
| return x, z, hs.to(torch.bfloat16), mk | |
| else: | |
| def get_batch(): | |
| idx = torch.randint(0, n, (args.batch,), device=DEV) | |
| return X[idx], Z[idx], HS[idx], MK[idx] | |
| hf_gen, hf_ae, _, bright0, contrast0 = sample_grid(start) | |
| print(f"[sample] шаг {start}: яркость {bright0:+.3f}, контраст {contrast0:.3f}, hf {hf_gen:.3f}", flush=True) | |
| t0 = time.perf_counter(); prev_el = t0 | |
| ema = {} | |
| run = {"lat": 0.0, "low": 0.0, "high": 0.0, "pix": 0.0, "n_low": 0, "n_high": 0, "n": 0} | |
| skips = 0 | |
| for step in range(start + 1, start + steps + 1): | |
| opt.zero_grad(set_to_none=True) | |
| ok = 0 | |
| for _ in range(args.accum): | |
| x0, z0, hs, mk = get_batch() | |
| # дропаут подписи (CFG) и дропаут слов внутри подписи | |
| if args.drop_text: | |
| drop = torch.rand(args.batch, device=DEV) < args.drop_text | |
| if drop.any(): # пустая подпись обрезается под длину батча | |
| L = hs.shape[1] | |
| hs = torch.where(drop[:, None, None, None], HS_E[:, :L].expand_as(hs), hs) | |
| mk = torch.where(drop[:, None], MK_E[:, :L].expand_as(mk), mk) | |
| if args.drop_word: | |
| word = (torch.rand(mk.shape, device=DEV) < args.drop_word) & mk | |
| hs = hs.masked_fill(word[:, :, None, None], 0) | |
| mk = mk & ~word | |
| with torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"): | |
| txt = enc.mix(hs.float()) | |
| tb = torch.rand(args.batch, device=DEV) | |
| eb = torch.randn_like(z0) | |
| zt = (1 - tb[:, None, None, None]) * z0 + tb[:, None, None, None] * eb | |
| z_hat = m.body(zt, tb, txt, mk) | |
| per_elem = (z_hat.float() - z0.float()).pow(2).mean(dim=(1, 2, 3)) | |
| low, high = tb < 0.3, tb > 0.7 | |
| loss_l = per_elem.mean() | |
| raw = m.to_raw(z0 + (z_hat - z0).detach()) | |
| tx = torch.rand(args.batch, device=DEV) | |
| ex = torch.randn_like(x0) | |
| xt = (1 - tx[:, None, None, None]) * x0 + tx[:, None, None, None] * ex | |
| xt_packed = pkx.pack([patchify(xt[i].to(ae.compute_dtype), 8) for i in range(args.batch)]) | |
| z_packed = pkx.pack([raw[i].flatten(1).transpose(0, 1) for i in range(args.batch)]) | |
| preds = ae.decoder(xt_packed, tx, z_packed, pkx) | |
| pix_terms = torch.stack([(p.float() - x0[i]).pow(2).mean() for i, p in enumerate(preds)]) | |
| loss_p = pix_terms.mean() | |
| total = loss_l + loss_p | |
| if not torch.isfinite(total): | |
| skips += 1 | |
| continue | |
| (total / args.accum).backward() | |
| ok += 1 | |
| run["lat"] += loss_l.item(); run["pix"] += loss_p.item() | |
| if low.any(): | |
| run["low"] += per_elem[low].mean().item(); run["n_low"] += 1 | |
| if high.any(): | |
| run["high"] += per_elem[high].mean().item(); run["n_high"] += 1 | |
| if not ok: | |
| print(f" ! шаг {step}: не-конечный лосс, пропуск (всего {skips})", flush=True) | |
| continue | |
| run["n"] += 1 | |
| torch.nn.utils.clip_grad_norm_(params, 1.0) | |
| lr = lr_at(step, args.lr, lr_min, args.lr_warmup, period, start) | |
| for g, base in zip(opt.param_groups, (args.lr, args.lr, args.lr_tail)): | |
| g["lr"] = lr * (base / args.lr) | |
| opt.step() | |
| if step % args.log_every == 0: | |
| d = args.ema_decay | |
| cur = {"lat": run["lat"] / run["n"], "pix": run["pix"] / run["n"], | |
| "low": run["low"] / max(run["n_low"], 1), "high": run["high"] / max(run["n_high"], 1)} | |
| for k, v in cur.items(): | |
| ema[k] = v if k not in ema else d * ema[k] + (1 - d) * v | |
| el = time.perf_counter() - t0; inc = el - prev_el; prev_el = el | |
| print(f" {step:6d} | mse_lat {cur['lat']:.4f} (ema {ema['lat']:.4f}; low {cur['low']:.3f} " | |
| f"high {cur['high']:.3f}) | mse_pix {cur['pix']:.4f} | lr {lr:.1e} | {el:6.0f}s " | |
| f"({inc / max(run['n'], 1) * 1e3:.0f} мс/шаг) | skip {skips}", flush=True) | |
| run = {"lat": 0.0, "low": 0.0, "high": 0.0, "pix": 0.0, "n_low": 0, "n_high": 0, "n": 0} | |
| if args.sample_every and step % args.sample_every == 0: | |
| zs = [] | |
| lat_pc_min = float("nan") | |
| for txt, mk in show_txt[:2]: | |
| z = torch.randn(1, 32, lat_hw[0], lat_hw[1], device=DEV, | |
| generator=torch.Generator(DEV).manual_seed(7)) | |
| with torch.no_grad(), torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"): | |
| for i in range(args.sample_steps): | |
| t = torch.full((1,), 1.0 - i / args.sample_steps, device=DEV) | |
| o = m.body(z, t, txt, mk).float() | |
| ou = m.body(z, t, enc.mix(HS_E.float()), MK_E).float() | |
| o = ou + args.cfg * (o - ou) | |
| z = z - (z - o) / t.clamp(min=1e-2)[:, None, None, None] / args.sample_steps | |
| zs.append(z) | |
| switch = (((zs[0] - zs[1]).norm() / zs[0].norm()).item() if len(zs) > 1 else float("nan")) | |
| if zs: | |
| lat_pc_min = float(zs[0].std(dim=(0, 2, 3)).min()) # сторож сжатия дисперсии латента | |
| hfg, hfa, canvas, bright, contrast = sample_grid(step) | |
| if show_x: | |
| z_ref = ae.encode(show_x[0]) | |
| tail_psnr = psnr(ae.decode_raw(m.to_raw(z_ref), steps=args.tail_steps, | |
| generator=torch.Generator(DEV).manual_seed(0)), show_x[0]) | |
| else: | |
| tail_psnr = float("nan") | |
| el = time.perf_counter() - t0 | |
| print(f" ↳ switch {switch:.3f} | яркость {bright:+.3f} контраст {contrast:.3f} | " | |
| f"латент: std {lat_pc_min:.2f} (у данных ~0.8–1.3) | hf {hfg:.3f} | tail(z0) {tail_psnr:.2f}", | |
| flush=True) | |
| with open(log_path, "a") as fh: | |
| fh.write(f"{step},{ema.get('lat', 0):.5f},{ema.get('low', 0):.5f},{ema.get('high', 0):.5f}," | |
| f"{ema.get('pix', 0):.5f},{switch:.4f},{bright:.4f},{contrast:.4f}," | |
| f"{lat_pc_min:.4f},{hfg:.4f},{tail_psnr:.2f},{el:.1f}\n") | |
| if args.save_every and step % args.save_every == 0: | |
| save(step) | |
| save(start + steps) | |
| if stream is not None: | |
| stream.stop = True # фоновые потоки не должны переживать интерпретатор | |
| print("[done]", flush=True) | |
| import os as _os | |
| _os._exit(0) # обойти шумную финализацию streaming-генераторов | |
| if __name__ == "__main__": | |
| main() | |