File size: 6,245 Bytes
b4f5f26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""Экспорт обученного UDM в `transformer/`: safetensors + config.json.

Три части в одном файле, по префиксам:
  body.*        — тело (8 блоков из энкодера AE + 4 новых), поток в латентном пространстве
  text_mixer.*  — обучаемый выбор слоёв Ettin (SliceMixer)
  tail.*        — декодер AE (рендер латента в пиксели), дообученный как обычный flow

  python3 src/export.py --ckpt tmp/run256/last.pt --out transformer
"""
from __future__ import annotations

import argparse
import json
from pathlib import Path

import torch
from safetensors.torch import save_file

ROOT = Path(__file__).resolve().parent.parent


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--ckpt", default=str(ROOT / "tmp" / "run256" / "last.pt"))
    ap.add_argument("--out", default=str(ROOT / "transformer"))
    ap.add_argument("--metrics", default="", help="дописать метрики в config.json (строка)")
    ap.add_argument("--half", action="store_true", help="хранить веса в fp16 (вдвое меньше, для публикации)")
    args = ap.parse_args()

    ck = torch.load(args.ckpt, map_location="cpu")
    tensors = {}
    for name, sd in (("body", ck["body"]), ("text_mixer", ck["mixer"]), ("tail", ck.get("decoder") or {})):
        for k, v in sd.items():
            tensors[f"{name}.{k}"] = (v.half() if args.half else v).contiguous()
    out = Path(args.out)
    out.mkdir(parents=True, exist_ok=True)
    save_file(tensors, str(out / "diffusion_pytorch_model.safetensors"))

    ae_cfg = json.loads((ROOT / "vae" / "config.json").read_text())
    ta = ck.get("args") or {}          # чекпойнт помнит свои аргументы — не сочиняем заново
    training = {
        "dataset": ta.get("hf_dataset") or ta.get("data") or "~/dataset/butterflies_256x64",
        "parts": ta.get("parts"),
        "size": ta.get("size"),
        "steps": int(ck["step"]),
        "batch": ta.get("batch", 8),
        "accum": ta.get("accum", 1),
        "lr": ta.get("lr", 1e-4),
        "lr_tail": ta.get("lr_tail", 3e-5),
        "drop_text": ta.get("drop_text", 0.1),
        "init": ta.get("init"),
        "metrics": args.metrics,
    }
    cfg = {
        "_class_name": "UDiT",
        "name": "Ra",
        "format": "udm/v1",
        "step": int(ck["step"]),
        "latent": {
            "channels": ae_cfg["latent_channels"],
            "spatial_factor": ae_cfg["patch_size"],
            "mean": ae_cfg["latent_mean"],
            "std": ae_cfg["latent_std"],
            "source": "vae/ (Muinez/f8c32-ae)",
        },
        "text": {
            "encoder": "text_encoder/ (jhu-clsp/ettin-encoder-150m, 768-dim, 22 слоя)",
            "max_len": 192,
            "slices": [2, 5, 8, 11, 14, 17, 20],
            "slice_blocks": 2,
            "trainable": "text_mixer.*",
        },
        "body": {
            "kind": "пирамидальный U: ширина растёт к низу",
            "levels": [
                {"level": 0, "tokens": "÷1", "width": 192, "blocks": "conv 6 enc / 4 dec + 1 self-attn на сторону",
                 "text": "кросс-внимание в каждом conv-блоке и в self-attn"},
                {"level": 1, "tokens": "÷2", "width": 512, "blocks": "attn 4 enc / 3 dec", "text": "кросс-внимание"},
                {"level": 2, "tokens": "÷4", "width": 1024, "blocks": "attn 3 enc / 2 dec", "text": "кросс-внимание"},
                {"level": 3, "tokens": "÷8", "width": 1536, "blocks": "attn 2 (боттлнек)", "text": "кросс-внимание"},
            ],
            "skips": "аддитивные (SD-стиль): e0/e1/e2 прибавляются в декодере",
            "down": "mean-pool 2× + Linear",
            "up": "Linear + nearest 2×",
            "rope": "2D RoPE, base 100, фаза в латентных пикселях на каждом уровне",
            "parametrization": "x0 латента",
        },
        "tail": {
            "source": "декодер AE, 6 блоков @512, PixelShuffle ×3",
            "trained_as": "flow по пикселям, условие z0 + (ошибка тела).detach()",
            "sample_steps": 4,
        },
        "params": {
            "body": round(sum(v.numel() for k, v in tensors.items() if k.startswith("body.")) / 1e6, 2),
            "text_mixer": round(sum(v.numel() for k, v in tensors.items()
                                    if k.startswith("text_mixer.")) / 1e6, 2),
            "tail": round(sum(v.numel() for k, v in tensors.items() if k.startswith("tail.")) / 1e6, 2),
        },
        "training": {k: v for k, v in training.items() if v is not None},
        "storage_dtype": "float16" if args.half else "float32",
    }
    cfg["params"]["total"] = round(sum(cfg["params"].values()), 2)
    (out / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))

    print(f"сохранено: {out}/diffusion_pytorch_model.safetensors "
          f"({sum(v.numel() for v in tensors.values())/1e6:.1f}M параметров, "
          f"{(out / 'diffusion_pytorch_model.safetensors').stat().st_size/1e6:.0f} МБ)")
    print(f"части: {cfg['params']}")


def load_exported(model, enc, ae, path, device=None):
    """Загрузить из `transformer/` (config.json + safetensors с префиксами body./text_mixer./tail.)."""
    import json
    from safetensors.torch import load_file
    path = Path(path)
    sd = load_file(str(path / "diffusion_pytorch_model.safetensors"), device=str(device) if device else None)
    def sub(prefix):
        return {k[len(prefix) + 1:]: v for k, v in sd.items() if k.startswith(prefix + ".")}
    model.body.load_state_dict(sub("body"), strict=False)
    enc.mixer.load_state_dict(sub("text_mixer"))
    ae.decoder.load_state_dict(sub("tail"))
    cfg_path = path / "config.json"
    return json.loads(cfg_path.read_text()) if cfg_path.exists() else {}


if __name__ == "__main__":
    main()