recoilme commited on
Commit
b4f5f26
·
verified ·
1 Parent(s): 5f344e9

Ra fp16 (step 212000)

Browse files
.gitattributes CHANGED
@@ -1,35 +1,9 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  *.pt filter=lfs diff=lfs merge=lfs -text
3
  *.pth filter=lfs diff=lfs merge=lfs -text
 
4
  *.safetensors filter=lfs diff=lfs merge=lfs -text
5
+ *.npy filter=lfs diff=lfs merge=lfs -text
6
+ *.parquet filter=lfs diff=lfs merge=lfs -text
7
+ *.png filter=lfs diff=lfs merge=lfs -text
8
+ *.jpg filter=lfs diff=lfs merge=lfs -text
9
+ docs/presentation.pdf filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # python
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # прогоны обучения, чекпойнты, сэмплы
6
+ tmp/
7
+ runs/
8
+ *.pt
9
+ nohup.out
10
+ *.log
11
+
12
+ # локальная постановка задачи (не для публикации)
13
+ task.md
14
+
15
+ # jupyter
16
+ .ipynb_checkpoints/
17
+
18
+ # кэши моделей и окружение
19
+ .hf_home/
20
+ .cache/
21
+ text_encoder/.cache/
22
+ .env
23
+
24
+ # системное
25
+ .DS_Store
docs/ARCH.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Архитектура
2
+
3
+ Три части, каждая со своей задачей. Всё, что ниже, проверено запуском.
4
+
5
+ ## 1. Тело: пирамидальный U-трансформер
6
+
7
+ Принцип: **ширина растёт там, где токенов мало.** Стоимость слоя ~ `токены × C²`, поэтому на
8
+ высоком разрешении держим узкие свёрточные блоки, а ширину и полное внимание — внизу.
9
+
10
+ | уровень | токенов при 512² | ширина | блоки | механика |
11
+ |---|---|---|---|---|
12
+ | L0 ÷1 | 4096 (512²) / 1024 (256²) | **192** | 6 enc / 4 dec + 1 self-attn с каждой стороны | depthwise 3×3 + expand 4× (ConvNeXt-стиль), кросс-внимание к тексту в каждом блоке |
13
+ | L1 ÷2 | 1024 / 256 | **512** | 4 enc / 3 dec | трансформер: self-attn + cross-attn к тексту + FFN |
14
+ | L2 ÷4 | 256 / 64 | **1024** | 3 enc / 2 dec | то же, полное внимание дёшево |
15
+ | L3 ÷8 | 64 / 16 | **1536** | 2 (боттлнек) | то же; широкий низ = глобальные ограничения (симметрия, композиция) |
16
+
17
+ Переходы и связи:
18
+
19
+ - **вниз**: mean-pool 2×2 + `Linear`, вверх: `Linear` + nearest 2×;
20
+ - **скипы аддитивные** (SD-стиль): выходы `e0/e1/e2` прибавляются к декодеру того же уровня;
21
+ ширина на входе и выходе уровня совпадает, поэтому сложение без проекций;
22
+ - **RoPE на каждом уровне**, фаза в **латентных пикселях** (L1 сдвинут вдвое, L2 вчетверо), поэтому
23
+ одна фаза означает одно расстояние независимо от уровня;
24
+ - **модуляция**: общий вектор `t + pooled-текст` → `(shift, scale, gate)` на уровень; в conv-блоках
25
+ L0 пять чанков (три на conv-путь, два на вход кросс-внимания);
26
+ - **zero-init**: выход conv-блоков и FFN обнулён, поэтому новые ветки не ломают старт.
27
+
28
+ Размеры: тело **143.3M**. Шаг обучения 97 мс при bs8 на 256² (8.1 ГБ). Для сравнения, прежний
29
+ плоский стек ширины 1024 на всех уровнях: 196 мс и hf 0.18 против 0.54 у пирамиды (архитектура
30
+ удалена в `tmp/archive`).
31
+
32
+ ## 2. Текст
33
+
34
+ - **Ettin-150M** (`text_encoder/`): ModernBERT, 768, 22 слоя, byte-level BPE, MIT, заморожен.
35
+ - **SliceMixer** (`src/text.py`, 10.04M обучаемых): 7 срезов (выходы после глобальных слоёв
36
+ 2, 5, 8, 11, 14, 17, 20) → пер-срезный LayerNorm (нормы растут 52 → 548, без нормировки ранние
37
+ слои не влияют) → 2 блока attention **по оси срезов** → `Linear(7→1)` → zero-init добавка к
38
+ среднему по срезам. Идея из `moss/adapter`, где 0.8B + 158M голова заменили 9B-энкодер.
39
+ - Вход в тело: кросс-внимание на **всех** уровнях (общие ключи/значения на уровень) плюс
40
+ pooled-текст в модуляции.
41
+
42
+ ## 3. Хвост
43
+
44
+ Декодер AE без изменений архитектуры: патчи 8×8, 6 блоков @512, adaLN с низкоранговыми дельтами,
45
+ long skip из блока n−2, финальный `PixelShuffle + conv3×3` ×3. 23.87M.
46
+
47
+ Обучается как обычный flow по пикселям: случайные `t`, случайный шум, цель — картинка, **условие —
48
+ `z0 + (ошибка тела).detach()`**. Условие без градиента: рендерер видит реальную ошибку тела, но не
49
+ может «научиться исправлять мусор» (именно это раньше увело его с 29.6 на 21.3 дБ).
50
+
51
+ ## 4. Сэмплинг
52
+
53
+ ```
54
+ z ~ N(0,1)
55
+ 4–8 шагов Эйлера по телу: x0 = тело(z, t, текст); z ← z − (z − x0)/(t·N)
56
+ CFG 1.5 (выше 2 при недообученной безусловной ветви ухудшает латент)
57
+ 1 + 3 шага декодера AE: изображение
58
+ ```
59
+
60
+ При 256² сэмпл стоит ~35 мс на 4800-й карте; при 512² — примерно вчетверо дороже.
61
+
62
+ ## 5. Бюджет
63
+
64
+ | часть | параметры |
65
+ |---|---|
66
+ | тело | 143.3M |
67
+ | выбор слоёв текста | 10.04M |
68
+ | хвост | 23.87M |
69
+ | **итого** | **177.3M** |
70
+
71
+ ## 6. Что зафиксировано и почему
72
+
73
+ - **x0, а не velocity.** Смена параметризации тела не затрагивает декодер (это отдельный flow со
74
+ своей целью), но требует переучивания тела с нуля — поэтому оставлено как есть.
75
+ - **Текст не экономим.** Кросс-внимание на L0 добавило 2.5M параметров и 9 мс, `switch` вырос
76
+ 0.43 → 0.78 при том же уровне деталей.
77
+ - **Никаких фиксированных шумов и клипов в обучении** — только стандартный flow-matching.
78
+ - **Удалено как тупиковое:** плоский 1024-стек, резидуальная U-ветка, Mix-FFN поверх плоского
79
+ стека, velocity-переключение посреди обучения, LPIPS в лоссе, кропы одной картинки вместо набора.
docs/README.ru.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ra (UDiT) — пирамидальный U-трансформер для text-to-image
2
+
3
+ Название модели — **Ra** (бог солнца, в честь пирамиды ширин 192 → 512 → 1024 → 1536);
4
+ `UDiT` — имя архитектуры в коде.
5
+
6
+ Одна модель из трёх частей: **тело** (пирамидальный U-трансформер, flow в латентном пространстве
7
+ AE), **хвост** (декодер AE, flow по пикселям) и **текст** (замороженный Ettin-150M плюс обучаемый
8
+ выбор слоёв). 177.3M параметров, обучение на 16 ГБ.
9
+
10
+ ```
11
+ текст: Ettin-150M (768, 22 слоя, заморожен)
12
+ └─ SliceMixer: 7 слоёв → attention по оси срезов → Linear(7→1) [10.04M]
13
+
14
+ шум (32, h, w) ─► ТЕЛО, пирамида ширины 192 → 512 → 1024 → 1536 [143.3M]
15
+ L0 ÷1 conv + текст 6 enc / 4 dec 4096 токенов при 512², 1024 при 256²
16
+ L1 ÷2 трансформер 4 / 3 1024 при 512², 256 при 256²
17
+ L2 ÷4 трансформер 3 / 2 256 / 64
18
+ L3 ÷8 трансформер 2 64 / 16 ← широкий низ, полное внимание
19
+ скипы аддитивные, вниз mean-pool 2×+Linear, вверх Linear+nearest 2×
20
+ ─► латент z (32, h, w) ─► ХВОСТ: декодер AE (6 блоков @512, PixelShuffle ×3) [23.87M]
21
+ flow по пикселям, условие z0 + (ошибка тела).detach()
22
+ ─► картинка
23
+ ```
24
+
25
+ ## Измерено (RTX 4080 16 ГБ, 256², bs8)
26
+
27
+ | | |
28
+ |---|---|
29
+ | шаг обучения | **97 мс** (8.1 ГБ), 121 обновление на эпоху при 970 картинках |
30
+ | параметры | тело 143.3M + текст 10.04M + хвост 23.87M = **177.3M** |
31
+ | бабочки, 20k шагов | `mse_lat` 0.107 (low 0.022, high 0.187), `mse_pix` 0.0105, `switch` 0.78, `tail(z0)` 24.7 дБ |
32
+ | детали (hf, Paper Kite) | **0.54** против AE 0.859 (63%); у прежней плоской 1024-архитектуры было 0.18 |
33
+ | потолок AE (256², бабочки) | hf 0.859 при GT 0.913 |
34
+
35
+ Подробности архитектуры — `docs/ARCH.md`, состояние и планы — `docs/STATUS.md`.
36
+
37
+ ## Состав
38
+
39
+ ```
40
+ src/model.py UDiT: тело-пирамида + хвост + sample()
41
+ src/text.py Ettin + SliceMixer (выбор слоёв)
42
+ src/train.py обучение на папке «картинка + .txt»
43
+ src/make_dataset.py сборка набора из плоской папки (бакеты кратны 64)
44
+ src/make_dataset_people.py сборка набора по классам из allphoto_train (woman/man/cat)
45
+ src/export.py экспорт в transformer/ (safetensors + config.json)
46
+ vae/ AE f8c32-ae: энкодер 68.4M + декодер 23.9M, f8, 32 канала
47
+ text_encoder/ Ettin-150M (768, 22 слоя, MIT)
48
+ transformer/ текущие веса модели + config.json
49
+ tmp/ прогоны (samples, log.csv, last.pt); tmp/archive — старое
50
+ docs/ARCH.md, docs/STATUS.md
51
+ ```
52
+
53
+ ## Запуск
54
+
55
+ ```bash
56
+ # данные: бакеты кратны 64 (тело трижды делит сетку на 2)
57
+ python3 src/make_dataset.py --src ~/dataset/butterflies/butterfly \
58
+ --out ~/dataset/butterflies_256x64 --long-side 256 --eval-n 30
59
+
60
+ # обучение (256², реальный батч 8, ~16 минут на 10k шагов)
61
+ python3 src/train.py --data ~/dataset/butterflies_256x64/train \
62
+ --eval ~/dataset/butterflies_256x64/eval --bs 8 --steps 20000 --every 500 --cfg 1.5
63
+
64
+ # продолжить с чекпойнта
65
+ python3 src/train.py ... --resume --out tmp/pyr2
66
+
67
+ # экспорт весов
68
+ python3 src/export.py --ckpt tmp/pyr2/last.pt --out transformer
69
+ ```
70
+
71
+ Смотреть прогресс: `tmp/<run>/samples/latest.png` (панели по промптам), `tmp/<run>/log.csv`.
72
+
73
+ ## Правила, выведенные из замеров
74
+
75
+ 1. **Ширина растёт к низу** (192 / 512 / 1024 / 1536). Плоский стек ширины 1024 на всех уровнях
76
+ стоил 196 мс/шаг и дал hf 0.18; пирамида при 97 мс даёт 0.54.
77
+ 2. **Текст — на всех уровнях**, включая самый мелкий: кросс-внимание в каждом conv-блоке L0 и
78
+ отдельный self-attn блок с текстом на L0. Стоило +2.5M параметров и +9 мс, `switch` вырос
79
+ 0.43 → 0.78.
80
+ 3. **Хвост учится на латенте `z0` плюс ошибка тела без градиента.** Обучение на предсказаниях тела
81
+ деградировало рендерер с 29.6 до 21.3 дБ; на чистом `z0` — рендерер не видит реальных ошибок.
82
+ 4. **Бакеты кратны 64**: три даунсэмпла требуют латента, кратного 8 (`h % 8 == 0`, `w % 8 == 0`).
83
+ Аспект — центр-кроп, не растяжение; в `allphoto_train` чёрные поля резать (`trim_black`).
84
+ 5. **Замер, а не впечатление.** В каждом логе: `mse_lat` (общий, low t<0.3, high t>0.7), `mse_pix`,
85
+ `switch` (один шум × два капшена), `tail(z0)` (сторож деградации хвоста), `skip` (не-конечные шаги).
86
+ Детали меряются долей высоких частот `hf(gen)/hf(AE.recon)`.
87
+ 6. **Цель — `x0`**, не velocity: обосновано только тем, что так наследуются веса декодера AE; смена
88
+ параметризации тела на декодер не влияет.
89
+ 7. **Никаких костылей в обучении** (фиксированный шум хвоста, клип латента, «mix» латентов):
90
+ всё, что влияет на результат, должно быть частью стандартного flow-matching.
docs/STATUS.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Состояние и планы
2
+
3
+ ## Что работает (проверено)
4
+
5
+ - **Пайплайн целиком**: текст → латент → пиксели, обучение и сэмплинг. Baseline-метрики в каждом
6
+ логе: `mse_lat` (общий, low t<0.3, high t>0.7), `mse_pix`, `switch`, `tail(z0)`, `skip`.
7
+ - **Бабочки, 20 000 шагов** (`butterflies_256x64`, 970 train / 30 eval, 97 мс/шаг):
8
+
9
+ | шаги | hf (доля от AE 0.859) | mse_lat | low | high | switch |
10
+ |---|---|---|---|---|---|
11
+ | 4 000 | 0.446 (52%) | 0.21 | 0.031 | 0.42 | 0.43 |
12
+ | 10 000 | 0.436 (51%) | 0.164 | 0.024 | 0.322 | 0.68 |
13
+ | 20 000 | **0.540 (63%)** | 0.107 | 0.022 | 0.187 | 0.78 |
14
+
15
+ Глазами: узнаваемая бабочка с жилками и рисунком, два сида дают две разные особи.
16
+ `tail(z0)` 24.7 дБ (потолок AE на 256² — 0.859 по hf), `skip` = 0.
17
+ - **Потолок AE не мешает**: на 256² AE сохраняет 0.859 из 0.913 высоких частот GT.
18
+
19
+ ## Чего ещё не сделано
20
+
21
+ 1. **Плато на бабочках не найдено.** hf 0.436 → 0.540 за 10k шагов, лоссы падают; 30k шагов
22
+ покажут, где насыщение.
23
+ 2. **Люди не обучены на этой архитектуре.** Готов билдер `src/make_dataset_people.py`
24
+ (5749 фото в `~/dataset/allphoto_train`, классы woman/man/cat, бакеты кратны 64).
25
+ Это главный тест: там капшены длинные и детальными, то есть деталь *определена условием*.
26
+ 3. **512² не запускался** на пирамиде. Нужен набор с бакетами кратны 64 (512×384, 512×256, 384×512,
27
+ 512×512) — `make_dataset.py --long-side 512 --multiple 64`. Ожидание: чешуйки/лица физически
28
+ разрешимы (бабочка 400 px вместо 200).
29
+ 4. **`high` = 0.187** — «выбор особи» на высоком шуме. Падает с обучением (0.42 → 0.19), поэтому
30
+ причина — недообучение, а не только бедность капшена.
31
+
32
+ ## Известные ограничения
33
+
34
+ - Латент AE — **f8** (64×64 при 512²). Всё мельче 8×8 px додумывает декодер, а не тело.
35
+ - Бакеты обязаны быть кратны **64** по каждой стороне (три даунсэмпла). При других размерах
36
+ даунсэмпл нецелый.
37
+ - Сравнение с прежними архитектурами корректно только на одинаковых наборах и шагах: старые
38
+ чекпойнты в новую архитектуру не переносятся (уровни и ширины другие).
39
+
40
+ ## Порядок работ
41
+
42
+ 1. Дожать бабочек до плато (30k) — 16 минут на 10k шагов.
43
+ 2. Перейти на людей (5749) при 256²: 30k шагов ≈ 50 минут.
44
+ 3. 512² на людях — после того, как на 256² будут лица/руки.
45
+ 4. Ёмкость (ширина боттлнека 1536 → 2048, больше блоков L0) — только если данные и разрешение
46
+ уже не дают прироста.
docs/img/samples.png ADDED

Git LFS Details

  • SHA256: 834a100446917c30ae20d0ad2094add61ce02038e07fb726c25876c46de8ca9a
  • Pointer size: 131 Bytes
  • Size of remote file: 672 kB
docs/img/samples_butterflies_20k.png ADDED

Git LFS Details

  • SHA256: c0d1ddbe7461912683070d89821923dca23a25ded65cfb652d96eb30d16b59a2
  • Pointer size: 131 Bytes
  • Size of remote file: 280 kB
docs/img/strip.png ADDED

Git LFS Details

  • SHA256: f60e39a5680e27439c94f05b5a29105449b155c6fccf645712f4ea57526b89a5
  • Pointer size: 131 Bytes
  • Size of remote file: 263 kB
docs/presentation.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e25cf92a5db610c2ff0ed13334eef2d22df88ae2d0378a3642afb4ace9fe65f6
3
+ size 953712
docs/presentation.src.html ADDED
@@ -0,0 +1,618 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru" data-register="technical">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>UDiT — пирамидальный U-трансформер</title>
6
+ <style>
7
+ @page { size: 1280px 720px; margin: 0; }
8
+ :root {
9
+ --paper: #f6f2ea; --ink: #14120f; --muted: #6d675c; --rule: #d9d2c3;
10
+ --copper: #a8551f; --teal: #1f6b66; --sand: #efe8da; --white: #fffdf8;
11
+ }
12
+ * { box-sizing: border-box; }
13
+ html, body { margin: 0; padding: 0; background: var(--paper); }
14
+ body { font-family: "DejaVu Sans", sans-serif; color: var(--ink); font-size: 15px; line-height: 1.45; }
15
+ .slide {
16
+ width: 1280px; height: 720px; padding: 54px 64px 44px; position: relative;
17
+ background: var(--paper); page-break-after: always; overflow: hidden;
18
+ }
19
+ .slide:last-child { page-break-after: auto; }
20
+ .eyebrow { font-family: "DejaVu Sans Mono", monospace; font-size: 11px; letter-spacing: .16em;
21
+ text-transform: uppercase; color: var(--copper); margin: 0 0 10px; }
22
+ h1 { font-family: 'P052', 'URW Bookman', serif; font-size: 46px; line-height: 1.08; margin: 0 0 14px; font-weight: 600; }
23
+ h2 { font-family: 'P052', 'URW Bookman', serif; font-size: 28px; line-height: 1.18; margin: 2px 0 8px; font-weight: 500; }
24
+ h3 { font-size: 14px; margin: 0 0 4px; letter-spacing: .02em; }
25
+ p { margin: 0 0 8px; }
26
+ .lede { font-size: 17px; color: #2c2822; max-width: 62ch; }
27
+ .muted { color: var(--muted); }
28
+ .mono { font-family: "DejaVu Sans Mono", monospace; font-variant-numeric: tabular-nums; }
29
+ .num { font-family: "DejaVu Sans Mono", monospace; font-weight: 700; }
30
+ .rule { height: 1px; background: var(--rule); margin: 18px 0 22px; }
31
+ .foot { position: absolute; left: 64px; right: 64px; bottom: 20px; display: flex;
32
+ justify-content: space-between; font-family: "DejaVu Sans Mono", monospace; font-size: 10.5px; color: var(--muted); }
33
+ .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
34
+ .grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
35
+ .grid4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
36
+ .card { background: var(--white); border: 1px solid var(--rule); padding: 16px 18px; }
37
+ .kpi { display: flex; gap: 26px; flex-wrap: wrap; }
38
+ .kpi div { border-left: 2px solid var(--copper); padding-left: 12px; }
39
+ .kpi b { display: block; font-family: "DejaVu Sans Mono", monospace; font-size: 23px; font-weight: 700; }
40
+ .kpi span { font-size: 12.5px; color: var(--muted); }
41
+ table { border-collapse: collapse; width: 100%; font-size: 14px; }
42
+ th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--rule); }
43
+ th { font-family: "DejaVu Sans Mono", monospace; font-size: 10.5px; letter-spacing: .1em;
44
+ text-transform: uppercase; color: var(--muted); font-weight: 400; }
45
+ td.n, th.n { text-align: right; font-family: "DejaVu Sans Mono", monospace; font-variant-numeric: tabular-nums; }
46
+ .tag { display: inline-block; font-family: "DejaVu Sans Mono", monospace; font-size: 11px;
47
+ padding: 1px 7px; border: 1px solid var(--rule); color: var(--muted); background: var(--sand); }
48
+ img.strip { width: 100%; display: block; border: 1px solid var(--rule); }
49
+ .caption { font-size: 12.5px; color: var(--muted); margin-top: 7px; }
50
+ .big { font-family: 'P052', serif; font-size: 21px; line-height: 1.3; }
51
+ ul { margin: 0; padding-left: 18px; }
52
+ li { margin-bottom: 5px; }
53
+ .accent { color: var(--copper); }
54
+ .teal { color: var(--teal); }
55
+ svg text { font-family: "DejaVu Sans", sans-serif; }
56
+ .mono-label { font-family: "DejaVu Sans Mono", monospace; font-size: 12px; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+
61
+ <!-- 1 ---------------------------------------------------------------- -->
62
+ <section class="slide">
63
+ <p class="eyebrow">text-to-image · 177.3M параметров · одна RTX 4080</p>
64
+ <h1>UDiT — пирамидальный U-трансформер</h1>
65
+ <p class="lede" style="max-width:88ch">Ширина растёт к низу: 192 → 512 → 1024 → 1536. Свёртки на мелком уровне,
66
+ внимание на грубом, текст на всех уровнях. Декодер готового автоэнкодера работает хвостом-рендерером,
67
+ поэтому тело считает 32 числа на клетку 8×8, а не пиксели.</p>
68
+
69
+ <div class="rule" style="margin:16px 0 14px"></div>
70
+ <div class="kpi">
71
+ <div><b>177.3M</b><span>параметров всего</span></div>
72
+ <div><b>97 мс</b><span>шаг обучения · bs8 · 256²</span></div>
73
+ <div><b>20.4</b><span>GFLOP forward у тела</span></div>
74
+ <div><b>0.647</b><span>детали (hf) против AE 0.859</span></div>
75
+ <div><b>8.1 ГБ</b><span>пик памяти из 16</span></div>
76
+ </div>
77
+
78
+ <img class="strip" src="img/strip.png" alt="оригинал, реконструкция AE и две генерации" style="margin-top:22px">
79
+ <p class="caption">слева направо: оригинал · реконструкция AE (потолок 0.859 hf) · две генерации модели на 30k шагов с разными сидами</p>
80
+
81
+ <div class="rule" style="margin:16px 0 10px"></div>
82
+ <div class="grid3">
83
+ <div><h3>тело · 143.3M</h3><p class="muted">пирамида 192/512/1024/1536, поток в латентном пространстве</p></div>
84
+ <div><h3>текст · 10.0M</h3><p class="muted">Ettin-150M заморожен, обучается только выбор слоёв</p></div>
85
+ <div><h3>хвост · 23.9M</h3><p class="muted">декодер AE, дообучается как flow по пикселям</p></div>
86
+ </div>
87
+
88
+ <div class="foot"><span>UDiT · архитектура</span><span>1 / 12</span></div>
89
+ </section>
90
+
91
+ <!-- 2 ---------------------------------------------------------------- -->
92
+ <section class="slide">
93
+ <p class="eyebrow">три части</p>
94
+ <h2>Модель не считает пиксели — только 32 числа на клетку 8×8</h2>
95
+
96
+ <svg width="1150" height="250" viewBox="0 0 1150 250" style="margin-top:16px">
97
+ <defs>
98
+ <marker id="a1" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
99
+ <path d="M0,0 L10,5 L0,10 z" fill="#14120f"/></marker>
100
+ <marker id="a2" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
101
+ <path d="M0,0 L10,5 L0,10 z" fill="#1f6b66"/></marker>
102
+ </defs>
103
+
104
+ <rect x="0" y="60" width="140" height="92" fill="#fffdf8" stroke="#d9d2c3"/>
105
+ <text x="14" y="90" font-size="15">шум</text>
106
+ <text x="14" y="114" class="mono-label" fill="#6d675c">32 × h/8 × w/8</text>
107
+ <text x="14" y="134" class="mono-label" fill="#6d675c">при 256²: 32×32</text>
108
+
109
+ <rect x="196" y="24" width="330" height="164" fill="#f2ece0" stroke="#a8551f"/>
110
+ <text x="212" y="54" font-size="16" font-weight="bold">ТЕЛО · пирамида</text>
111
+ <text x="212" y="80" class="mono-label" fill="#6d675c">143.3M · 20.4 GFLOP</text>
112
+ <text x="212" y="102" class="mono-label" fill="#6d675c">4–8 шагов Эйлера · CFG 1.5</text>
113
+ <rect x="212" y="116" width="298" height="12" fill="#c98a5f"/><text x="212" y="146" class="mono-label" fill="#7c3d14">÷1 · 192</text>
114
+ <rect x="212" y="150" width="220" height="12" fill="#a8551f"/><text x="440" y="161" class="mono-label" fill="#7c3d14">÷2 · 512</text>
115
+ <rect x="212" y="168" width="140" height="12" fill="#7c3d14"/><text x="360" y="179" class="mono-label" fill="#7c3d14">÷4 · 1024 · ÷8 · 1536</text>
116
+
117
+ <rect x="582" y="60" width="150" height="92" fill="#fffdf8" stroke="#d9d2c3"/>
118
+ <text x="596" y="90" font-size="15">латент</text>
119
+ <text x="596" y="114" class="mono-label" fill="#6d675c">32 × h/8 × w/8</text>
120
+ <text x="596" y="134" class="mono-label" fill="#6d675c">x0-предсказание</text>
121
+
122
+ <rect x="788" y="24" width="230" height="164" fill="#e8f0ee" stroke="#1f6b66"/>
123
+ <text x="802" y="54" font-size="16" font-weight="bold">ХВОСТ · декодер AE</text>
124
+ <text x="802" y="80" class="mono-label" fill="#6d675c">23.9M · 4 шага flow</text>
125
+ <text x="802" y="102" class="mono-label" fill="#6d675c">PixelShuffle ×3 → 3 канала</text>
126
+ <text x="802" y="124" class="mono-label" fill="#6d675c">условие: z0 + ошибка тела</text>
127
+ <text x="802" y="146" class="mono-label" fill="#6d675c">обучен заранее, как декодер AE</text>
128
+ <text x="802" y="168" class="mono-label" fill="#6d675c">дообучается вместе с телом</text>
129
+
130
+ <rect x="1074" y="60" width="76" height="92" fill="#fffdf8" stroke="#d9d2c3"/>
131
+ <text x="1084" y="90" font-size="15">px</text>
132
+ <text x="1084" y="114" class="mono-label" fill="#6d675c">3×H×W</text>
133
+
134
+ <line x1="144" y1="106" x2="192" y2="106" stroke="#14120f" stroke-width="1.8" marker-end="url(#a1)"/>
135
+ <line x1="530" y1="106" x2="578" y2="106" stroke="#14120f" stroke-width="1.8" marker-end="url(#a1)"/>
136
+ <line x1="736" y1="106" x2="784" y2="106" stroke="#14120f" stroke-width="1.8" marker-end="url(#a1)"/>
137
+ <line x1="1022" y1="106" x2="1070" y2="106" stroke="#14120f" stroke-width="1.8" marker-end="url(#a1)"/>
138
+
139
+ <rect x="196" y="210" width="822" height="38" fill="#fffdf8" stroke="#1f6b66"/>
140
+ <text x="210" y="234" font-size="13.5">ТЕКСТ · Ettin-150M (768, заморожен) + SliceMixer 10.04M · кросс-внимание в теле на всех четырёх уровнях</text>
141
+ <line x1="300" y1="208" x2="300" y2="192" stroke="#1f6b66" stroke-width="1.8" marker-end="url(#a2)"/>
142
+ </svg>
143
+
144
+ <div class="grid3" style="margin-top:14px">
145
+ <div><h3>Латент вместо пикселей</h3><p class="muted">Один токен = 32 числа на клетку 8×8. При 256² это 1024 токена против 196 608 пикселей.</p></div>
146
+ <div><h3>Декодер уже обучен</h3><p class="muted">Хвост наследует декодер AE: телу не нужно учиться рисовать текстуру с нуля.</p></div>
147
+ <div><h3>Текст не экономим</h3><p class="muted">Кросс-внимание в каждом блоке до самого мелкого уровня: +2.5M, «switch» 0.43 → 0.78.</p></div>
148
+ </div>
149
+
150
+ <div class="foot"><span>UDiT · три части</span><span>2 / 12</span></div>
151
+ </section>
152
+
153
+ <!-- 3 ---------------------------------------------------------------- -->
154
+ <section class="slide">
155
+ <p class="eyebrow">почему пирамида</p>
156
+ <h2>Плоский стек ширины 1024 платит за каждый токен по полной</h2>
157
+
158
+ <div class="grid2" style="margin-top:18px; gap:44px">
159
+ <div>
160
+ <svg width="530" height="212" viewBox="0 0 530 212">
161
+ <text x="0" y="12" font-size="13.5" font-weight="bold" fill="#a8551f">было: 12 блоков × ширина 1024</text>
162
+ <g fill="#e0b39a">
163
+ <rect x="0" y="26" width="360" height="9"/><rect x="0" y="39" width="360" height="9"/>
164
+ <rect x="0" y="52" width="360" height="9"/><rect x="0" y="65" width="360" height="9"/>
165
+ <rect x="0" y="78" width="360" height="9"/><rect x="0" y="91" width="360" height="9"/>
166
+ <rect x="0" y="104" width="360" height="9"/><rect x="0" y="117" width="360" height="9"/>
167
+ <rect x="0" y="130" width="360" height="9"/><rect x="0" y="143" width="360" height="9"/>
168
+ </g>
169
+ <line x1="372" y1="26" x2="372" y2="152" stroke="#a8551f"/>
170
+ <text x="380" y="70" class="mono-label" fill="#7c3d14">все блоки</text>
171
+ <text x="380" y="88" class="mono-label" fill="#7c3d14">на одном</text>
172
+ <text x="380" y="106" class="mono-label" fill="#7c3d14">разрешении</text>
173
+ <text x="0" y="176" class="mono-label" fill="#6d675c">токенов на блоке: 1024 · ширина: 1024 · уровней: 1</text>
174
+ <text x="0" y="196" class="mono-label" fill="#a8551f">232 GFLOP · 196 мс/шаг · hf 0.18</text>
175
+ <text x="0" y="212" class="mono-label" fill="#6d675c">нет уровня, где вся картинка — это 16 токенов</text>
176
+ </svg>
177
+ </div>
178
+ <div>
179
+ <svg width="530" height="212" viewBox="0 0 530 212">
180
+ <text x="0" y="12" font-size="13.5" font-weight="bold" fill="#1f6b66">стало: 192 → 512 → 1024 → 1536</text>
181
+ <rect x="0" y="26" width="400" height="26" fill="#c98a5f"/>
182
+ <text x="10" y="43" class="mono-label" fill="#fffdf8">÷1 · 1024 токена · ширина 192 · свёртки</text>
183
+ <rect x="60" y="58" width="310" height="26" fill="#b06f3f"/>
184
+ <text x="70" y="75" class="mono-label" fill="#fffdf8">÷2 · 256 токенов · 512</text>
185
+ <rect x="120" y="90" width="220" height="26" fill="#a8551f"/>
186
+ <text x="130" y="107" class="mono-label" fill="#fffdf8">÷4 · 64 · 1024</text>
187
+ <rect x="180" y="122" width="130" height="26" fill="#7c3d14"/>
188
+ <text x="190" y="139" class="mono-label" fill="#fffdf8">÷8 · 16 · 1536</text>
189
+ <text x="0" y="176" class="mono-label" fill="#6d675c">энкодер вверх, декодер вниз, аддитивный скип на каждом уровне</text>
190
+ <text x="0" y="196" class="mono-label" fill="#1f6b66">20.4 GFLOP · 97 мс/шаг · hf 0.54 → 0.65</text>
191
+ <text x="0" y="212" class="mono-label" fill="#6d675c">широкий низ: 16 токенов видят всю картинку целиком</text>
192
+ </svg>
193
+ </div>
194
+ </div>
195
+
196
+ <div class="rule" style="margin:6px 0 10px"></div>
197
+ <table>
198
+ <tr><th style="width:32%">метрика</th><th class="n" style="width:13%">плоский 1024</th><th class="n" style="width:12%">пирамида</th><th>что это значит</th></tr>
199
+ <tr><td>GFLOP на forward (тело)</td><td class="n">232</td><td class="n">20.4</td><td>в 11 раз меньше арифметики при том же числе параметров</td></tr>
200
+ <tr><td>шаг обучения, bs8 · 256²</td><td class="n">196 мс</td><td class="n">97 мс</td><td>вдвое больше шагов за то же время</td></tr>
201
+ <tr><td>детали: hf(gen)/hf(AE)</td><td class="n">21%</td><td class="n">51–75%</td><td>и это при 3–6 кратном меньшем числе шагов обучения</td></tr>
202
+ </table>
203
+
204
+ <div class="rule" style="margin:10px 0 12px"></div>
205
+ <p class="big">Стоимость слоя ≈ токены × ширина². Мы держим ширину там, где токенов мало, и снимаем
206
+ с мелкого уровня 28-кратный перерасход — эти же деньги уходят в глубину и разрешение.</p>
207
+
208
+ <div class="foot"><span>UDiT · сравнение</span><span>3 / 12</span></div>
209
+ </section>
210
+
211
+ <!-- 4 ---------------------------------------------------------------- -->
212
+ <section class="slide">
213
+ <p class="eyebrow">��естница</p>
214
+ <h2>Ширина растёт там, где токенов мало — стоимость уровней сбалансирована</h2>
215
+
216
+ <div class="grid2" style="margin-top:12px; grid-template-columns: 1.25fr 1fr;">
217
+ <div>
218
+ <table>
219
+ <tr><th>уровень</th><th class="n">токенов 256²</th><th class="n">токенов 512²</th><th class="n">ширина</th><th>блоки</th><th>механика</th></tr>
220
+ <tr><td>L0 ÷1</td><td class="n">1024</td><td class="n">4096</td><td class="n">192</td><td>6 enc / 4 dec + self-attn</td><td>свёртки + текст</td></tr>
221
+ <tr><td>L1 ÷2</td><td class="n">256</td><td class="n">1024</td><td class="n">512</td><td>4 / 3</td><td>трансформер</td></tr>
222
+ <tr><td>L2 ÷4</td><td class="n">64</td><td class="n">256</td><td class="n">1024</td><td>3 / 2</td><td>трансформер</td></tr>
223
+ <tr><td>L3 ÷8</td><td class="n">16</td><td class="n">64</td><td class="n">1536</td><td>2 (боттлнек)</td><td>трансформер</td></tr>
224
+ </table>
225
+ <p class="caption">энкодер поднимается по лестнице, декодер спускается; на каждом уровне прибавляется скип</p>
226
+ </div>
227
+ <div>
228
+ <svg width="470" height="290" viewBox="0 0 470 290">
229
+ <text x="0" y="14" font-size="12" fill="#6d675c">стоимость уровня ≈ токены × ширина²</text>
230
+ <g>
231
+ <rect x="0" y="30" width="240" height="26" fill="#c98a5f"/><text x="250" y="48" class="mono-label">37.7 · L0</text>
232
+ <rect x="0" y="66" width="427" height="26" fill="#b06f3f"/><text x="437" y="84" class="mono-label">67.1 · L1</text>
233
+ <rect x="0" y="102" width="427" height="26" fill="#a8551f"/><text x="437" y="120" class="mono-label">67.1 · L2</text>
234
+ <rect x="0" y="138" width="240" height="26" fill="#7c3d14"/><text x="250" y="156" class="mono-label">37.7 · L3</text>
235
+ </g>
236
+ <line x1="0" y1="182" x2="430" y2="182" stroke="#d9d2c3"/>
237
+ <text x="0" y="204" font-size="12" fill="#a8551f">для сравнения: 1024 на всех уровнях</text>
238
+ <rect x="0" y="214" width="470" height="26" fill="#e7c8b2"/>
239
+ <text x="8" y="232" class="mono-label" fill="#7c3d14">1073 · L0 при ширине 1024 (в 28 раз дороже)</text>
240
+ <text x="0" y="272" class="mono-label" fill="#6d675c">параметры уезжают вниз, где токенов 16, а не 1024</text>
241
+ </svg>
242
+ </div>
243
+ </div>
244
+
245
+ <div class="foot"><span>UDiT · бюджет уровней</span><span>4 / 12</span></div>
246
+ </section>
247
+
248
+ <!-- 5 ---------------------------------------------------------------- -->
249
+ <section class="slide">
250
+ <p class="eyebrow">роли уровней</p>
251
+ <h2>Каждый уровень отвечает за свой масштаб — как у художника</h2>
252
+
253
+ <div class="grid4" style="margin-top:18px">
254
+ <div class="card">
255
+ <svg width="100%" height="96" viewBox="0 0 220 96">
256
+ <rect x="18" y="14" width="184" height="68" fill="none" stroke="#d9d2c3"/>
257
+ <circle cx="110" cy="48" r="26" fill="#a8551f" opacity="0.22"/>
258
+ <circle cx="110" cy="48" r="12" fill="#a8551f" opacity="0.55"/>
259
+ <text x="70" y="92" class="mono-label" fill="#6d675c">16 токенов</text>
260
+ </svg>
261
+ <h3>L3 · «прищур»</h3>
262
+ <p class="muted">Вся картинка — 16 токенов. Полное внимание: все видят всех. Здесь решается композиция и симметрия.</p>
263
+ </div>
264
+ <div class="card">
265
+ <svg width="100%" height="96" viewBox="0 0 220 96">
266
+ <rect x="18" y="14" width="184" height="68" fill="none" stroke="#d9d2c3"/>
267
+ <path d="M60,70 Q110,10 160,70" fill="none" stroke="#1f6b66" stroke-width="3"/>
268
+ <text x="60" y="92" class="mono-label" fill="#6d675c">64 токена</text>
269
+ </svg>
270
+ <h3>L2 · крупные формы</h3>
271
+ <p class="muted">Тело, крылья, общая раскраска. Ширина 1024 — запас «смысла» на грубое описание.</p>
272
+ </div>
273
+ <div class="card">
274
+ <svg width="100%" height="96" viewBox="0 0 220 96">
275
+ <rect x="18" y="14" width="184" height="68" fill="none" stroke="#d9d2c3"/>
276
+ <path d="M40,60 C70,20 150,20 180,60" fill="none" stroke="#1f6b66" stroke-width="2"/>
277
+ <path d="M40,70 C70,34 150,34 180,70" fill="none" stroke="#1f6b66" stroke-width="2" opacity="0.6"/>
278
+ <text x="60" y="92" class="mono-label" fill="#6d675c">256 токенов</text>
279
+ </svg>
280
+ <h3>L1 · средние детали</h3>
281
+ <p class="muted">Форма крыла, крупный рисунок, где тёмное и где светлое. Ширина 512.</p>
282
+ </div>
283
+ <div class="card">
284
+ <svg width="100%" height="96" viewBox="0 0 220 96">
285
+ <rect x="18" y="14" width="184" height="68" fill="none" stroke="#d9d2c3"/>
286
+ <g stroke="#a8551f" stroke-width="1" opacity="0.8">
287
+ <line x1="30" y1="30" x2="46" y2="46"/><line x1="70" y1="26" x2="86" y2="42"/>
288
+ <line x1="110" y1="30" x2="126" y2="46"/><line x1="150" y1="26" x2="166" y2="42"/>
289
+ <line x1="40" y1="60" x2="56" y2="76"/><line x1="90" y1="58" x2="106" y2="74"/>
290
+ <line x1="140" y1="60" x2="156" y2="76"/>
291
+ </g>
292
+ <text x="56" y="92" class="mono-label" fill="#6d675c">1024 токена</text>
293
+ </svg>
294
+ <h3>L0 · текстура</h3>
295
+ <p class="muted">Жилки, кромки, крапинки. Здесь свёртка 3×3, а не внимание: текстура локальна, а свёртка в 20 раз дешевле.</p>
296
+ </div>
297
+ </div>
298
+
299
+ <div class="rule"></div>
300
+ <p class="big">Внимание на грубом уровне стоит копейки: 16 токенов × 16 = 256 пар. Внимание на мелком — 1024 × 1024 = миллион пар. Поэтому «прищур» мы делаем вниманием, а текстуру — свёрткой.</p>
301
+
302
+ <div class="foot"><span>UDiT · роли уровней</span><span>5 / 12</span></div>
303
+ </section>
304
+
305
+ <!-- 6 ---------------------------------------------------------------- -->
306
+ <section class="slide">
307
+ <p class="eyebrow">путь и скипы</p>
308
+ <h2>Энкодер поднимается к смыслу, декодер возвращается к деталям — помня эскиз</h2>
309
+
310
+ <svg width="1150" height="300" viewBox="0 0 1150 300" style="margin-top:14px">
311
+ <defs>
312
+ <marker id="up" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
313
+ <path d="M0,0 L10,5 L0,10 z" fill="#a8551f"/></marker>
314
+ <marker id="dn" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
315
+ <path d="M0,0 L10,5 L0,10 z" fill="#1f6b66"/></marker>
316
+ </defs>
317
+
318
+ <g>
319
+ <rect x="0" y="196" width="210" height="66" fill="#fffdf8" stroke="#d9d2c3"/>
320
+ <text x="14" y="222" font-size="13.5">L0 · 1024 токена · 192</text>
321
+ <text x="14" y="244" class="mono-label" fill="#6d675c">6 блоков enc · 4 dec</text>
322
+
323
+ <rect x="272" y="140" width="210" height="66" fill="#fffdf8" stroke="#d9d2c3"/>
324
+ <text x="286" y="166" font-size="13.5">L1 · 256 · 512</text>
325
+ <text x="286" y="188" class="mono-label" fill="#6d675c">4 enc · 3 dec</text>
326
+
327
+ <rect x="544" y="84" width="210" height="66" fill="#fffdf8" stroke="#d9d2c3"/>
328
+ <text x="558" y="110" font-size="13.5">L2 · 64 · 1024</text>
329
+ <text x="558" y="132" class="mono-label" fill="#6d675c">3 enc · 2 dec</text>
330
+
331
+ <rect x="816" y="28" width="210" height="66" fill="#fffdf8" stroke="#d9d2c3"/>
332
+ <text x="830" y="54" font-size="13.5">L3 · 16 · 1536</text>
333
+ <text x="830" y="76" class="mono-label" fill="#6d675c">2 блока · полное внимание</text>
334
+ </g>
335
+
336
+ <g stroke="#a8551f" stroke-width="2" fill="none">
337
+ <line x1="212" y1="228" x2="266" y2="180" marker-end="url(#up)"/>
338
+ <line x1="484" y1="172" x2="538" y2="124" marker-end="url(#up)"/>
339
+ <line x1="756" y1="116" x2="810" y2="68" marker-end="url(#up)"/>
340
+ </g>
341
+ <text x="206" y="212" class="mono-label" fill="#a8551f">pool 2×</text>
342
+ <text x="478" y="156" class="mono-label" fill="#a8551f">pool 2×</text>
343
+ <text x="750" y="100" class="mono-label" fill="#a8551f">pool 2×</text>
344
+
345
+ <g stroke="#1f6b66" stroke-width="2" fill="none">
346
+ <line x1="866" y1="98" x2="792" y2="146" marker-end="url(#dn)"/>
347
+ <line x1="594" y1="154" x2="520" y2="202" marker-end="url(#dn)"/>
348
+ <line x1="322" y1="210" x2="248" y2="258" marker-end="url(#dn)"/>
349
+ </g>
350
+ <text x="880" y="120" class="mono-label" fill="#1f6b66">up 2×</text>
351
+ <text x="608" y="176" class="mono-label" fill="#1f6b66">up 2×</text>
352
+ <text x="336" y="232" class="mono-label" fill="#1f6b66">up 2×</text>
353
+
354
+ <rect x="0" y="286" width="1026" height="0" fill="none"/>
355
+ <text x="0" y="292" class="mono-label" fill="#a8551f">скип: на каждом уровне выход энкодера прибавляется ко входу декодера того же уровня (аддитивно, без проекций)</text>
356
+ </svg>
357
+
358
+ <div class="grid3" style="margin-top:10px">
359
+ <div><h3>Почему аддитивные</h3><p class="muted">Ширина входа и выхода уровня совпадает, проекции не нужны — так делают U-Net’ы SD.</p></div>
360
+ <div><h3>Почему pool, а не stride</h3><p class="muted">Среднее 2×2 плюс линейный слой: сжатие учится и не ломает фазу позиционного кодирования.</p></div>
361
+ <div><h3>Что это даёт</h3><p class="muted">Тонкая структура строится на своём уровне, а не вытягивается ��ниманием с одного масштаба.</p></div>
362
+ </div>
363
+
364
+ <div class="foot"><span>UDiT · путь и скипы</span><span>6 / 12</span></div>
365
+ </section>
366
+
367
+ <!-- 7 ---------------------------------------------------------------- -->
368
+ <section class="slide">
369
+ <p class="eyebrow">текст</p>
370
+ <h2>Подпись не «берётся из последнего слоя» — модель сама выбирает, какие слои читать</h2>
371
+
372
+ <div class="grid2" style="margin-top:14px; grid-template-columns: 1.15fr 1fr;">
373
+ <div>
374
+ <svg width="560" height="252" viewBox="0 0 560 252">
375
+ <text x="0" y="12" font-size="11.5" fill="#6d675c">норма растёт с глубиной в 10 раз — без пер-слойной нормировки ранние слои не влияют</text>
376
+ <g class="mono-label" fill="#6d675c">
377
+ <text x="0" y="42">слой 2</text><text x="0" y="66">слой 5</text><text x="0" y="90">слой 8</text>
378
+ <text x="0" y="114">слой 11</text><text x="0" y="138">слой 14</text><text x="0" y="162">слой 17</text>
379
+ <text x="0" y="186">слой 20</text>
380
+ </g>
381
+ <g fill="#c98a5f">
382
+ <rect x="70" y="30" width="44" height="12"/><rect x="70" y="54" width="47" height="12"/>
383
+ <rect x="70" y="78" width="58" height="12"/><rect x="70" y="102" width="84" height="12"/>
384
+ <rect x="70" y="126" width="187" height="12"/><rect x="70" y="150" width="346" height="12"/>
385
+ </g>
386
+ <rect x="70" y="174" width="403" height="12" fill="#a8551f"/>
387
+ <text x="120" y="26" class="mono-label" fill="#7c3d14">52</text>
388
+ <text x="480" y="170" class="mono-label" fill="#7c3d14">548</text>
389
+ <rect x="0" y="206" width="560" height="24" fill="#e8f0ee" stroke="#1f6b66"/>
390
+ <text x="10" y="223" class="mono-label" fill="#1f6b66">7 срезов → LN на каждый → внимание по срезам → Linear(7→1) → + среднее</text>
391
+ <text x="0" y="248" class="mono-label" fill="#6d675c">«проектор слоёв»: какие слои читать, решает обучение, а не догадка</text>
392
+ </svg>
393
+ <p class="caption">SliceMixer · 10.04M обучаемых · идея из moss/adapter (0.8B + 158M голова вместо 9B-энкодера)</p>
394
+ </div>
395
+ <div>
396
+ <div class="card" style="margin-bottom:14px">
397
+ <h3>Два входа текста в каждый блок</h3>
398
+ <ul class="muted">
399
+ <li>кросс-внимание: каждый токен-клетка спрашивает «что в подписи про моё место?»</li>
400
+ <li>модуляция: общий вектор «t + усреднённый текст» настраивает shift/scale/gate фильтров</li>
401
+ </ul>
402
+ </div>
403
+ <table>
404
+ <tr><th>что сделали</th><th class="n">цена</th><th class="n">эффект</th></tr>
405
+ <tr><td>кросс-внимание и на L0</td><td class="n">+2.5M</td><td class="n">+9 мс</td></tr>
406
+ <tr><td>«switch» (один шум, два капшена)</td><td class="n">—</td><td class="n">0.43 → 0.78</td></tr>
407
+ <tr><td>детали (hf) не пострадали</td><td class="n">—</td><td class="n">0.44 → 0.65</td></tr>
408
+ </table>
409
+ <p class="caption">«switch» — насколько латент меняется от смены подписи при том же шуме</p>
410
+ </div>
411
+ </div>
412
+
413
+ <div class="foot"><span>UDiT · текст</span><span>7 / 12</span></div>
414
+ </section>
415
+
416
+ <!-- 8 ---------------------------------------------------------------- -->
417
+ <section class="slide">
418
+ <p class="eyebrow">хвост и сэмплинг</p>
419
+ <h2>Тело ведёт латент, готовый декодер рисует пиксели</h2>
420
+
421
+ <svg width="1140" height="240" viewBox="0 0 1140 240" style="margin-top:12px">
422
+ <defs>
423
+ <marker id="a3" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">
424
+ <path d="M0,0 L9,4.5 L0,9 z" fill="#14120f"/></marker>
425
+ </defs>
426
+ <g>
427
+ <circle cx="40" cy="70" r="18" fill="#fffdf8" stroke="#d9d2c3"/><text x="34" y="75" class="mono-label">шум</text>
428
+ <line x1="60" y1="70" x2="120" y2="70" stroke="#14120f" marker-end="url(#a3)"/>
429
+ <rect x="126" y="44" width="180" height="52" fill="#f2ece0" stroke="#a8551f"/>
430
+ <text x="138" y="66" font-size="12">тело: x0-предсказание</text>
431
+ <text x="138" y="84" class="mono-label" fill="#6d675c">8 шагов · CFG 1.5</text>
432
+ <line x1="310" y1="70" x2="370" y2="70" stroke="#14120f" marker-end="url(#a3)"/>
433
+ <rect x="376" y="44" width="150" height="52" fill="#fffdf8" stroke="#d9d2c3"/>
434
+ <text x="388" y="66" font-size="12">латент z</text>
435
+ <text x="388" y="84" class="mono-label" fill="#6d675c">32 × h/8 × w/8</text>
436
+ <line x1="530" y1="70" x2="590" y2="70" stroke="#14120f" marker-end="url(#a3)"/>
437
+ <rect x="596" y="44" width="190" height="52" fill="#e8f0ee" stroke="#1f6b66"/>
438
+ <text x="608" y="66" font-size="12">хвост: декодер AE</text>
439
+ <text x="608" y="84" class="mono-label" fill="#6d675c">4 шага flow</text>
440
+ <line x1="790" y1="70" x2="850" y2="70" stroke="#14120f" marker-end="url(#a3)"/>
441
+ <rect x="856" y="44" width="130" height="52" fill="#fffdf8" stroke="#d9d2c3"/>
442
+ <text x="868" y="66" font-size="12">пиксели</text>
443
+ <text x="868" y="84" class="mono-label" fill="#6d675c">3 × H × W</text>
444
+ </g>
445
+ </svg>
446
+
447
+ <div class="rule" style="margin:6px 0 10px"></div>
448
+ <table style="table-layout:fixed">
449
+ <tr><th style="width:30%">на каком латенте учить хвост</th><th style="width:18%">что видит рендерер</th><th style="width:52%">что происходит</th></tr>
450
+ <tr><td>только z0 (чистый латент)</td><td>идеальный латент</td><td>на сэмплинге встречает ошибку тела, к которой не привык</td></tr>
451
+ <tr><td>только предсказание тела</td><td>мусор в начале обучения</td><td>учится «исправлять мусор» и разучивается рисовать: 29.6 → 21.3 дБ</td></tr>
452
+ <tr><td><b>z0 + (ошибка тела).detach()</b></td><td>реальную ошибку</td><td>видит промах, но градиент через него не идёт; 24.7–25.9 дБ стабильно</td></tr>
453
+ </table>
454
+ <p class="caption">Сэмпл: 8 шагов тела + 4 шага декодера ≈ 35 мс на 4080 при 256². CFG 1.5 — выше 2 при недоученной
455
+ безусловной ветви ухудшает латент (замерено 0.19 → 0.28). Метрика tail(z0) в каждом логе ловит деградацию рендерера.</p>
456
+
457
+ <div class="foot"><span>UDiT · хвост</span><span>8 / 12</span></div>
458
+ </section>
459
+
460
+ <!-- 9 ---------------------------------------------------------------- -->
461
+ <section class="slide">
462
+ <p class="eyebrow">результат на узком домене</p>
463
+ <h2>Бабочки: детали растут с шагами и не насыщаются</h2>
464
+
465
+ <div class="grid2" style="margin-top:10px; grid-template-columns: 1fr 1.1fr;">
466
+ <div>
467
+ <table>
468
+ <tr><th>шаги</th><th class="n">hf / AE</th><th class="n">mse_lat</th><th class="n">high</th><th class="n">switch</th></tr>
469
+ <tr><td>4 000</td><td class="n">0.446 · 52%</td><td class="n">0.21</td><td class="n">0.42</td><td class="n">0.43</td></tr>
470
+ <tr><td>10 000</td><td class="n">0.436 · 51%</td><td class="n">0.164</td><td class="n">0.32</td><td class="n">0.68</td></tr>
471
+ <tr><td>20 000</td><td class="n">0.540 · 63%</td><td class="n">0.107</td><td class="n">0.19</td><td class="n">0.78</td></tr>
472
+ <tr><td><b>30 000</b></td><td class="n"><b>0.647 · 75%</b></td><td class="n"><b>0.072</b></td><td class="n"><b>0.114</b></td><td class="n">0.78</td></tr>
473
+ </table>
474
+ <p class="caption">hf — энергия высоких частот (лапласиан). AE-реконструкция того же вида: 0.859, оригинал 0.913.
475
+ «high» — ошибка на высоком шуме, то есть «какую особь рисовать»; она упала в 3.7 раза.</p>
476
+ <p class="big" style="margin-top:10px">Три сида дают три разные особи: 0.642 / 0.646 / 0.654 — модель не сколлапсировала в один мод.</p>
477
+ </div>
478
+ <div>
479
+ <img class="strip" src="img/samples.png" alt="Четыре вида бабочек, сгенерированные моделью">
480
+ <p class="caption">генерации на 30k шагов: Delias, Cepora, Troides, Catasticta — вид задаётся подписью из 16 токенов</p>
481
+ </div>
482
+ </div>
483
+
484
+ <div class="foot"><span>UDiT · результат</span><span>9 / 12</span></div>
485
+ </section>
486
+
487
+ <!-- 10 --------------------------------------------------------------- -->
488
+ <section class="slide">
489
+ <p class="eyebrow">цена</p>
490
+ <h2>177.3M параметров, 97 мс на шаг, 8.1 ГБ — обучение на одной карте</h2>
491
+
492
+ <div class="grid3" style="margin-top:16px">
493
+ <div>
494
+ <h3>Параметры по частям</h3>
495
+ <svg width="330" height="170" viewBox="0 0 330 170">
496
+ <rect x="0" y="20" width="240" height="24" fill="#a8551f"/><text x="248" y="37" class="mono-label">143.3 M · тело</text>
497
+ <rect x="0" y="54" width="17" height="24" fill="#1f6b66"/><text x="25" y="71" class="mono-label">10.0 M · текст</text>
498
+ <rect x="0" y="88" width="40" height="24" fill="#6d675c"/><text x="48" y="105" class="mono-label">23.9 M · хвост</text>
499
+ <line x1="0" y1="132" x2="300" y2="132" stroke="#d9d2c3"/>
500
+ <text x="0" y="152" class="mono-label" fill="#6d675c">всего 177.3M · чекпойнт 709 МБ</text>
501
+ </svg>
502
+ </div>
503
+ <div>
504
+ <h3>Арифметика на forward</h3>
505
+ <svg width="330" height="170" viewBox="0 0 330 170">
506
+ <rect x="0" y="20" width="20" height="24" fill="#1f6b66"/><text x="28" y="37" class="mono-label">20.4 · пирамида</text>
507
+ <rect x="0" y="54" width="230" height="24" fill="#e7c8b2"/><text x="238" y="71" class="mono-label" fill="#7c3d14">232 · плоский 1024</text>
508
+ <text x="0" y="106" class="mono-label" fill="#6d675c">+ хвост ≈ 29 GFLOP</text>
509
+ <text x="0" y="126" class="mono-label" fill="#6d675c">итого ≈ 50 против ≈ 262</text>
510
+ <text x="0" y="152" class="mono-label" fill="#6d675c">та же карта, в 5 раз больше шагов</text>
511
+ </svg>
512
+ </div>
513
+ <div>
514
+ <h3>Скорость и память</h3>
515
+ <svg width="330" height="170" viewBox="0 0 330 170">
516
+ <rect x="0" y="20" width="110" height="24" fill="#a8551f"/><text x="118" y="37" class="mono-label">97 мс · пирамида</text>
517
+ <rect x="0" y="54" width="222" height="24" fill="#e7c8b2"/><text x="230" y="71" class="mono-label" fill="#7c3d14">196 мс · плоский</text>
518
+ <text x="0" y="106" class="mono-label" fill="#6d675c">8.1 ГБ из 16 (bs8, 256²)</text>
519
+ <text x="0" y="126" class="mono-label" fill="#6d675c">8-битный AdamW: −1 ГБ состояний</text>
520
+ <text x="0" y="152" class="mono-label" fill="#6d675c">эффективность ~1.6 TFLOPS — упор в память</text>
521
+ </svg>
522
+ </div>
523
+ </div>
524
+
525
+ <div class="rule"></div>
526
+ <div class="grid2">
527
+ <div class="card">
528
+ <h3>Почему хватает 16 ГБ</h3>
529
+ <p class="muted">Память ест не модель (2.5 ГБ весов и состояний), а активации: они ~ токены × ширина × блоки.
530
+ Пирамида держит широкие слои на 16–64 токенах, поэтому активации маленькие.</p>
531
+ </div>
532
+ <div class="card">
533
+ <h3>Где запас</h3>
534
+ <p class="muted">Шаг упирается в память, а не в счёт. Запас есть на батч 16 (14.8 ГБ с хвостом) и на 512²
535
+ с батчем 1–2 — то есть на разрешение, где деталь физически возможна.</p>
536
+ </div>
537
+ </div>
538
+
539
+ <div class="foot"><span>UDiT · цена</span><span>10 / 12</span></div>
540
+ </section>
541
+
542
+ <!-- 11 --------------------------------------------------------------- -->
543
+ <section class="slide">
544
+ <p class="eyebrow">следующий прогон</p>
545
+ <h2>FLUX-Reason: 2 эпохи по 1.5M картинок, поток без кэша</h2>
546
+
547
+ <div class="grid2" style="margin-top:14px">
548
+ <div>
549
+ <table>
550
+ <tr><th>параметр</th><th>значение</th></tr>
551
+ <tr><td>датасет</td><td>LucasFang/FLUX-Reason-6M, часть Aesthetics-Part01 (415 шардов, ~250 ГБ)</td></tr>
552
+ <tr><td>разрешение / батч</td><td>256² · bs16</td></tr>
553
+ <tr><td>подпись</td><td>caption_entity → caption_composition (40–400 симв.), иначе caption_detail</td></tr>
554
+ <tr><td>бюджет</td><td>2 эпохи = 187 500 шагов ≈ 12.5 ч</td></tr>
555
+ <tr><td>оптимизатор</td><td>AdamW8bit, прогрев 500, cosine до 10%</td></tr>
556
+ <tr><td>дропауты</td><td>подпись 10% (CFG) · слова 5%</td></tr>
557
+ <tr><td>старт</td><td>веса бабочек (30k шагов)</td></tr>
558
+ </table>
559
+ </div>
560
+ <div>
561
+ <h3>Что уже видно (шаг 800)</h3>
562
+ <svg width="480" height="150" viewBox="0 0 480 150">
563
+ <text x="0" y="18" class="mono-label" fill="#6d675c">mse_lat общий и по зонам шума</text>
564
+ <line x1="0" y1="120" x2="470" y2="120" stroke="#d9d2c3"/>
565
+ <rect x="10" y="60" width="60" height="60" fill="#a8551f"/><text x="18" y="140" class="mono-label">0.33</text>
566
+ <rect x="110" y="113" width="60" height="7" fill="#1f6b66"/><text x="120" y="140" class="mono-label">0.037</text>
567
+ <rect x="210" y="120" width="60" height="0.5" fill="#6d675c"/>
568
+ <text x="330" y="70" class="mono-label" fill="#6d675c">low (t&lt;0.3): 0.037</text>
569
+ <text x="330" y="90" class="mono-label" fill="#6d675c">high (t&gt;0.7): 0.70</text>
570
+ <text x="330" y="110" class="mono-label" fill="#6d675c">241 мс/шаг · 66 картинок/с</text>
571
+ <text x="330" y="130" class="mono-label" fill="#6d675c">9.5 ГБ · skip 0</text>
572
+ </svg>
573
+ <p class="caption">«high» здесь в 6 раз выше, чем на бабочках: 1.5M разных сцен, «какую именно рисовать»
574
+ по подписи не угадать — это нормальный старт, а не поломка.</p>
575
+ </div>
576
+ </div>
577
+
578
+ <div class="rule"></div>
579
+ <div class="grid3">
580
+ <div><h3>Смотреть</h3><p class="muted">tmp/flux/log.csv · tmp/flux/samples/latest.png (лиса, стеклянный город, неон, импрессионист)</p></div>
581
+ <div><h3>Остановить / продолжить</h3><p class="muted">kill процесса · тот же запуск с --resume --out tmp/flux</p></div>
582
+ <div><h3>Честно про «2 эпохи»</h3><p class="muted">эпоха по Part01 = 1.5M картинок; все 4 части ≈ 1 ТБ, а на диске 352 ГБ</p></div>
583
+ </div>
584
+
585
+ <div class="foot"><span>UDiT · FLUX-Reason</span><span>11 / 12</span></div>
586
+ </section>
587
+
588
+ <!-- 12 --------------------------------------------------------------- -->
589
+ <section class="slide">
590
+ <p class="eyebrow">правила</p>
591
+ <h2>Всё, что ниже — вывод из замеров, а не из вкуса</h2>
592
+
593
+ <div class="grid2" style="margin-top:16px; gap:26px 40px">
594
+ <div>
595
+ <p><span class="tag">1</span> <b>Ширина растёт к низу.</b> <span class="muted">Плоский 1024 на всех уровнях: 196 мс и hf 0.18. Пирамида: 97 мс и 0.54.</span></p>
596
+ <p><span class="tag">2</span> <b>Текст не экономим.</b> <span class="muted">Кросс-внимание на самом мелком уровне: +2.5M и +9 мс, «switch» 0.43 → 0.78.</span></p>
597
+ <p><span class="tag">3</span> <b>Хвост учится на z0 + ошибка тела.</b> <span class="muted">Только z0 — не видит реальных ошибок; только предсказания тела — деградация 29.6 → 21.3 дБ.</span></p>
598
+ </div>
599
+ <div>
600
+ <p><span class="tag">4</span> <b>Бакеты кратны 64.</b> <span class="muted">Три даунсэмпла требуют латента, кратного 8. Аспект — центр-кроп, не растяжение.</span></p>
601
+ <p><span class="tag">5</span> <b>Метрики вместо впечатлений.</b> <span class="muted">mse_lat (общий, low, high), mse_pix, switch, tail(z0), hf(gen)/hf(AE), skip.</span></p>
602
+ <p><span class="tag">6</span> <b>Никаких костылей.</b> <span class="muted">Фиксированный шум, клип латента, «mix» латентов — выброшены: они прячут то, что мы измеряем.</span></p>
603
+ </div>
604
+ </div>
605
+
606
+ <div class="rule"></div>
607
+ <div class="grid4">
608
+ <div><h3>Дальше</h3><p class="muted">дожать FLUX-Reason до 2 эпох по Part01</p></div>
609
+ <div><h3>Потом</h3><p class="muted">люди: 5749 фото, 256² — лица и кисти</p></div>
610
+ <div><h3>Затем</h3><p class="muted">512² на людях: деталь физически разрешима</p></div>
611
+ <div><h3>Только потом</h3><p class="muted">ёмкость: ширина боттлнека и число блоков L0</p></div>
612
+ </div>
613
+
614
+ <div class="foot"><span>UDiT · правила</span><span>12 / 12</span></div>
615
+ </section>
616
+
617
+ </body>
618
+ </html>
src/export.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Экспорт обученного UDM в `transformer/`: safetensors + config.json.
2
+
3
+ Три части в одном файле, по префиксам:
4
+ body.* — тело (8 блоков из энкодера AE + 4 новых), поток в латентном пространстве
5
+ text_mixer.* — обучаемый выбор слоёв Ettin (SliceMixer)
6
+ tail.* — декодер AE (рендер латента в пиксели), дообученный как обычный flow
7
+
8
+ python3 src/export.py --ckpt tmp/run256/last.pt --out transformer
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import torch
17
+ from safetensors.torch import save_file
18
+
19
+ ROOT = Path(__file__).resolve().parent.parent
20
+
21
+
22
+ def main():
23
+ ap = argparse.ArgumentParser()
24
+ ap.add_argument("--ckpt", default=str(ROOT / "tmp" / "run256" / "last.pt"))
25
+ ap.add_argument("--out", default=str(ROOT / "transformer"))
26
+ ap.add_argument("--metrics", default="", help="дописать метрики в config.json (строка)")
27
+ ap.add_argument("--half", action="store_true", help="хранить веса в fp16 (вдвое меньше, для публикации)")
28
+ args = ap.parse_args()
29
+
30
+ ck = torch.load(args.ckpt, map_location="cpu")
31
+ tensors = {}
32
+ for name, sd in (("body", ck["body"]), ("text_mixer", ck["mixer"]), ("tail", ck.get("decoder") or {})):
33
+ for k, v in sd.items():
34
+ tensors[f"{name}.{k}"] = (v.half() if args.half else v).contiguous()
35
+ out = Path(args.out)
36
+ out.mkdir(parents=True, exist_ok=True)
37
+ save_file(tensors, str(out / "diffusion_pytorch_model.safetensors"))
38
+
39
+ ae_cfg = json.loads((ROOT / "vae" / "config.json").read_text())
40
+ ta = ck.get("args") or {} # чекпойнт помнит свои аргументы — не сочиняем заново
41
+ training = {
42
+ "dataset": ta.get("hf_dataset") or ta.get("data") or "~/dataset/butterflies_256x64",
43
+ "parts": ta.get("parts"),
44
+ "size": ta.get("size"),
45
+ "steps": int(ck["step"]),
46
+ "batch": ta.get("batch", 8),
47
+ "accum": ta.get("accum", 1),
48
+ "lr": ta.get("lr", 1e-4),
49
+ "lr_tail": ta.get("lr_tail", 3e-5),
50
+ "drop_text": ta.get("drop_text", 0.1),
51
+ "init": ta.get("init"),
52
+ "metrics": args.metrics,
53
+ }
54
+ cfg = {
55
+ "_class_name": "UDiT",
56
+ "name": "Ra",
57
+ "format": "udm/v1",
58
+ "step": int(ck["step"]),
59
+ "latent": {
60
+ "channels": ae_cfg["latent_channels"],
61
+ "spatial_factor": ae_cfg["patch_size"],
62
+ "mean": ae_cfg["latent_mean"],
63
+ "std": ae_cfg["latent_std"],
64
+ "source": "vae/ (Muinez/f8c32-ae)",
65
+ },
66
+ "text": {
67
+ "encoder": "text_encoder/ (jhu-clsp/ettin-encoder-150m, 768-dim, 22 слоя)",
68
+ "max_len": 192,
69
+ "slices": [2, 5, 8, 11, 14, 17, 20],
70
+ "slice_blocks": 2,
71
+ "trainable": "text_mixer.*",
72
+ },
73
+ "body": {
74
+ "kind": "пирамидальный U: ширина растёт к низу",
75
+ "levels": [
76
+ {"level": 0, "tokens": "÷1", "width": 192, "blocks": "conv 6 enc / 4 dec + 1 self-attn на сторону",
77
+ "text": "кросс-внимание в каждом conv-блоке и в self-attn"},
78
+ {"level": 1, "tokens": "÷2", "width": 512, "blocks": "attn 4 enc / 3 dec", "text": "кросс-внимание"},
79
+ {"level": 2, "tokens": "÷4", "width": 1024, "blocks": "attn 3 enc / 2 dec", "text": "кросс-внимание"},
80
+ {"level": 3, "tokens": "÷8", "width": 1536, "blocks": "attn 2 (боттлнек)", "text": "кросс-внимание"},
81
+ ],
82
+ "skips": "аддитивные (SD-стиль): e0/e1/e2 прибавляются в декодере",
83
+ "down": "mean-pool 2× + Linear",
84
+ "up": "Linear + nearest 2×",
85
+ "rope": "2D RoPE, base 100, фаза в латентных пикселях на каждом уровне",
86
+ "parametrization": "x0 латента",
87
+ },
88
+ "tail": {
89
+ "source": "декодер AE, 6 блоков @512, PixelShuffle ×3",
90
+ "trained_as": "flow по пикселям, условие z0 + (ошибка тела).detach()",
91
+ "sample_steps": 4,
92
+ },
93
+ "params": {
94
+ "body": round(sum(v.numel() for k, v in tensors.items() if k.startswith("body.")) / 1e6, 2),
95
+ "text_mixer": round(sum(v.numel() for k, v in tensors.items()
96
+ if k.startswith("text_mixer.")) / 1e6, 2),
97
+ "tail": round(sum(v.numel() for k, v in tensors.items() if k.startswith("tail.")) / 1e6, 2),
98
+ },
99
+ "training": {k: v for k, v in training.items() if v is not None},
100
+ "storage_dtype": "float16" if args.half else "float32",
101
+ }
102
+ cfg["params"]["total"] = round(sum(cfg["params"].values()), 2)
103
+ (out / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
104
+
105
+ print(f"сохранено: {out}/diffusion_pytorch_model.safetensors "
106
+ f"({sum(v.numel() for v in tensors.values())/1e6:.1f}M параметров, "
107
+ f"{(out / 'diffusion_pytorch_model.safetensors').stat().st_size/1e6:.0f} МБ)")
108
+ print(f"части: {cfg['params']}")
109
+
110
+
111
+ def load_exported(model, enc, ae, path, device=None):
112
+ """Загрузить из `transformer/` (config.json + safetensors с префиксами body./text_mixer./tail.)."""
113
+ import json
114
+ from safetensors.torch import load_file
115
+ path = Path(path)
116
+ sd = load_file(str(path / "diffusion_pytorch_model.safetensors"), device=str(device) if device else None)
117
+ def sub(prefix):
118
+ return {k[len(prefix) + 1:]: v for k, v in sd.items() if k.startswith(prefix + ".")}
119
+ model.body.load_state_dict(sub("body"), strict=False)
120
+ enc.mixer.load_state_dict(sub("text_mixer"))
121
+ ae.decoder.load_state_dict(sub("tail"))
122
+ cfg_path = path / "config.json"
123
+ return json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
124
+
125
+
126
+ if __name__ == "__main__":
127
+ main()
src/make_dataset.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Сборка набора из папки «картинка + .txt»: бакеты (кратны --multiple) и центр-кроп.
2
+
3
+ python3 src/make_dataset.py --src ~/dataset/butterflies/butterfly \
4
+ --out ~/dataset/butterflies_256 --long-side 256 --eval-n 30
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import csv
10
+ import math
11
+ import random
12
+ from pathlib import Path
13
+
14
+ from PIL import Image, ImageOps
15
+
16
+ BUCKETS = {}
17
+
18
+
19
+ def buckets(long_side: int, multiple: int = 16) -> dict:
20
+ L = long_side
21
+ def r16(x): return max(multiple, int(round(x / multiple)) * multiple)
22
+ return {
23
+ "1:1": (L, L),
24
+ "4:3": (L, r16(L * 3 / 4)),
25
+ "3:4": (r16(L * 3 / 4), L),
26
+ "3:2": (L, r16(L * 2 / 3)),
27
+ "2:3": (r16(L * 2 / 3), L),
28
+ "16:9": (L, r16(L * 9 / 16)),
29
+ "9:16": (r16(L * 9 / 16), L),
30
+ }
31
+
32
+
33
+ def plan_size(w, h, max_upscale=1.4):
34
+ ratio = w / h
35
+ label, (tw, th) = min(BUCKETS.items(), key=lambda kv: abs(math.log(ratio / (kv[1][0] / kv[1][1]))))
36
+ if ratio > tw / th:
37
+ cw, ch = round(h * tw / th), h
38
+ else:
39
+ cw, ch = w, round(w * th / tw)
40
+ cw, ch = min(cw, w), min(ch, h)
41
+ if cw * max_upscale < tw or ch * max_upscale < th:
42
+ return None
43
+ return label, tw, th, cw, ch
44
+
45
+
46
+ def main():
47
+ ap = argparse.ArgumentParser()
48
+ ap.add_argument("--src", required=True)
49
+ ap.add_argument("--out", required=True)
50
+ ap.add_argument("--long-side", type=int, default=256)
51
+ ap.add_argument("--eval-n", type=int, default=30)
52
+ ap.add_argument("--max-upscale", type=float, default=1.4)
53
+ ap.add_argument("--multiple", type=int, default=64,
54
+ help="кратность сторон: три даунсэмпла тела требуют латент, кратный 8")
55
+ ap.add_argument("--seed", type=int, default=0)
56
+ args = ap.parse_args()
57
+
58
+ global BUCKETS
59
+ BUCKETS = buckets(args.long_side, args.multiple)
60
+ src, out = Path(args.src), Path(args.out)
61
+ for sub in ("train", "eval"):
62
+ (out / sub).mkdir(parents=True, exist_ok=True)
63
+
64
+ pairs = []
65
+ for img in sorted(list(src.glob("*.png")) + list(src.glob("*.jpg"))):
66
+ txt = img.with_suffix(".txt")
67
+ if txt.exists():
68
+ pairs.append((img, txt))
69
+ random.Random(args.seed).shuffle(pairs)
70
+ print(f"пар: {len(pairs)}")
71
+
72
+ rows, kept = [], 0
73
+ for i, (img, txt) in enumerate(pairs):
74
+ im = ImageOps.exif_transpose(Image.open(img)).convert("RGB")
75
+ plan = plan_size(im.width, im.height, args.max_upscale)
76
+ if plan is None:
77
+ continue
78
+ label, tw, th, cw, ch = plan
79
+ left, top = (im.width - cw) // 2, (im.height - ch) // 2
80
+ im = im.crop((left, top, left + cw, top + ch)).resize((tw, th), Image.LANCZOS)
81
+ split = "eval" if i < args.eval_n else "train"
82
+ name = f"{img.stem}"
83
+ im.save(out / split / f"{name}.jpg", quality=95)
84
+ (out / split / f"{name}.txt").write_text(txt.read_text().strip() + "\n")
85
+ rows.append({"split": split, "name": name, "w": tw, "h": th, "bucket": label})
86
+ kept += 1
87
+ with open(out / "index.csv", "w", newline="") as fh:
88
+ w = csv.DictWriter(fh, fieldnames=["split", "name", "w", "h", "bucket"])
89
+ w.writeheader(); w.writerows(rows)
90
+ print(f"готово: {kept} (train {kept-args.eval_n}, eval {args.eval_n}) -> {out}")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
src/make_dataset_people.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Сборка набора по классам (woman/man/cat) из ~/dataset/allphoto_train — тест анатомии.
2
+
3
+ Зачем: бабочки однородны и анатомию не показывают. Нужен набор, где модель обязана выучить
4
+ тело: разные люди, позы, ракурсы, и капшены, которые называют, кто это и что делает.
5
+
6
+ Классы: woman / man / cat — по N уникальных капшенов на класс (дедуп по первым 60 символам,
7
+ иначе один photoshoot даёт десятки почти одинаковых кадров).
8
+
9
+ python3 src/make_dataset_people.py --n 60 --out ~/dataset/testset_anatomy
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import csv
15
+ import math
16
+ import random
17
+ import re
18
+ import shutil
19
+ from pathlib import Path
20
+
21
+ from PIL import Image, ImageOps
22
+
23
+ SRC = Path.home() / "dataset" / "allphoto_train"
24
+
25
+ CLASSES = {
26
+ "woman": ("woman", "women", "girl"),
27
+ "man": ("man", "men", "boy"),
28
+ "cat": ("cat",),
29
+ }
30
+ # бакеты как в moss/src/make_dataset.py: обе стороны кратны 16 (латент /8, потом patch-2),
31
+ # соотношение выбирается ближайшим по |log(ratio_источника / ratio_бакета)|, затем ЦЕНТР-КРОП —
32
+ # растягивать нельзя, иначе лица и тела деформируются
33
+ def buckets(long_side: int = 512, multiple: int = 64) -> dict:
34
+ """Бакеты: длинная сторона long_side, обе стороны кратны 16 (латент /8, затем /2)."""
35
+ L = long_side
36
+ def r16(x): return max(multiple, int(round(x / multiple)) * multiple)
37
+ return {
38
+ "1:1": (L, L),
39
+ "4:3": (L, r16(L * 3 / 4)),
40
+ "3:4": (r16(L * 3 / 4), L),
41
+ "3:2": (L, r16(L * 2 / 3)),
42
+ "2:3": (r16(L * 2 / 3), L),
43
+ "16:9": (L, r16(L * 9 / 16)),
44
+ "9:16": (r16(L * 9 / 16), L),
45
+ }
46
+ BUCKETS = buckets(512)
47
+
48
+ # для людей требуем, чтобы в кадре было тело, а не только лицо: бабочки-портреты анатомию не учат
49
+ BODY_WORDS = ("standing", "walking", "sitting", "seated", "jeans", "shorts", "dress", "skirt",
50
+ "shirt", "legs", "arms", "hands", "wearing", "full body", "walks", "stands")
51
+
52
+
53
+ def classify(cap: str) -> str | None:
54
+ low = cap.lower()
55
+ for cls, words in CLASSES.items():
56
+ if any(re.search(rf"\b{w}\b", low) for w in words):
57
+ # «man» внутри «woman» уже отсечён \b; но «woman» не должна становиться «man»
58
+ if cls == "man" and re.search(r"\bwoman\b", low):
59
+ return "woman"
60
+ return cls
61
+ return None
62
+
63
+
64
+ def trim_black(im: Image.Image, thr: float = 12.0, var: float = 8.0) -> Image.Image:
65
+ """Срезает однородные чёрные поля по краям (в allphoto_train их полно: 71 из 171 кадра)."""
66
+ import numpy as np
67
+ a = np.asarray(im.convert("L"), dtype=np.float32)
68
+ def uniform_edge(line): # чёрная и без структуры -> рамка, а не ночная сцена
69
+ return line.mean() < thr and line.std() < var
70
+ top, bot, left, right = 0, a.shape[0], 0, a.shape[1]
71
+ while top < bot - 8 and uniform_edge(a[top]): top += 1
72
+ while bot > top + 8 and uniform_edge(a[bot - 1]): bot -= 1
73
+ while left < right - 8 and uniform_edge(a[top:bot, left]): left += 1
74
+ while right > left + 8 and uniform_edge(a[top:bot, right - 1]): right -= 1
75
+ return im.crop((left, top, right, bot)) if (left or top or right != a.shape[1] or bot != a.shape[0]) else im
76
+
77
+
78
+ def plan_size(w: int, h: int, max_upscale: float = 1.4):
79
+ """(w,h) -> (label, tw, th, cw, ch) или None, если источника не хватает (как в moss)."""
80
+ ratio = w / h
81
+ label, (tw, th) = min(BUCKETS.items(), key=lambda kv: abs(math.log(ratio / (kv[1][0] / kv[1][1]))))
82
+ if ratio > tw / th:
83
+ cw, ch = round(h * tw / th), h
84
+ else:
85
+ cw, ch = w, round(w * th / tw)
86
+ cw, ch = min(cw, w), min(ch, h)
87
+ if cw * max_upscale < tw or ch * max_upscale < th:
88
+ return None
89
+ return label, tw, th, cw, ch
90
+
91
+
92
+ def load_bucketed(path: Path, max_upscale: float = 1.4):
93
+ """EXIF-поворот -> срез чёрных полей -> ближайший бакет -> центр-кроп -> ресайз."""
94
+ im = ImageOps.exif_transpose(Image.open(path)).convert("RGB")
95
+ im = trim_black(im)
96
+ plan = plan_size(im.width, im.height, max_upscale)
97
+ if plan is None:
98
+ return None, None
99
+ label, tw, th, cw, ch = plan
100
+ left, top = (im.width - cw) // 2, (im.height - ch) // 2
101
+ return im.crop((left, top, left + cw, top + ch)).resize((tw, th), Image.LANCZOS), label
102
+
103
+
104
+ def main():
105
+ ap = argparse.ArgumentParser()
106
+ ap.add_argument("--n", type=int, default=60, help="картинок на класс")
107
+ ap.add_argument("--eval-n", type=int, default=4, help="сколько на класс отложить для показа")
108
+ ap.add_argument("--max-upscale", type=float, default=1.4)
109
+ ap.add_argument("--long-side", type=int, default=512)
110
+ ap.add_argument("--multiple", type=int, default=64)
111
+ ap.add_argument("--seed", type=int, default=0)
112
+ ap.add_argument("--out", default=str(Path.home() / "dataset" / "testset_anatomy"))
113
+ args = ap.parse_args()
114
+
115
+ global BUCKETS
116
+ BUCKETS = buckets(args.long_side, args.multiple)
117
+ out = Path(args.out)
118
+ for sub in ("train", "eval"):
119
+ (out / sub).mkdir(parents=True, exist_ok=True)
120
+
121
+ caps = sorted(SRC.glob("*.txt"))
122
+ rng = random.Random(args.seed)
123
+ rng.shuffle(caps)
124
+
125
+ seen_prefix: set[str] = set()
126
+ picked: dict[str, list[tuple[Path, str]]] = {c: [] for c in CLASSES}
127
+ need = args.n + args.eval_n
128
+ for f in caps:
129
+ cap = " ".join(f.read_text().split())
130
+ if len(cap) < 40:
131
+ continue
132
+ cls = classify(cap)
133
+ if cls is None or len(picked[cls]) >= need:
134
+ continue
135
+ low = cap.lower()
136
+ if cls == "cat" and re.search(r"\b(dog|puppy|kitten|rabbit)\b", low) and "cat" not in low:
137
+ continue
138
+ if cls in ("woman", "man") and not any(w in low for w in BODY_WORDS):
139
+ continue
140
+ key = cap[:60].lower()
141
+ if key in seen_prefix:
142
+ continue
143
+ img = f.with_suffix(".jpg")
144
+ if not img.exists():
145
+ continue
146
+ seen_prefix.add(key)
147
+ picked[cls].append((img, cap))
148
+ if all(len(v) >= need for v in picked.values()):
149
+ break
150
+
151
+ rows = []
152
+ for cls, items in picked.items():
153
+ for i, (img, cap) in enumerate(items):
154
+ split = "eval" if i < args.eval_n else "train"
155
+ im, bucket = load_bucketed(img)
156
+ if im is None:
157
+ continue
158
+ name = f"{cls}_{i:03d}"
159
+ im.save(out / split / f"{name}.jpg", quality=92)
160
+ (out / split / f"{name}.txt").write_text(cap + "\n")
161
+ rows.append({"split": split, "class": cls, "name": name, "w": im.width, "h": im.height,
162
+ "bucket": bucket, "caption": cap})
163
+ print(f" {split}/{name} {im.width}x{im.height} | {cap[:80]}")
164
+
165
+ with open(out / "index.csv", "w", newline="") as fh:
166
+ w = csv.DictWriter(fh, fieldnames=["split", "class", "name", "w", "h", "bucket", "caption"])
167
+ w.writeheader(); w.writerows(rows)
168
+ per = {c: sum(1 for r in rows if r["class"] == c and r["split"] == "train") for c in CLASSES}
169
+ print(f"\nитого: {sum(per.values())} train {per} + {sum(1 for r in rows if r['split']=='eval')} eval"
170
+ f" -> {out}")
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
src/model.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """UDiT — пирамидальный U-трансформер: тело (латентный поток) + хвост (декодер AE).
2
+
3
+ Идея: ширина растёт там, где токенов мало. Узко на высоком разрешении, широко внизу,
4
+ как в U-Net'ах SD (320/640/1280 при 64/32/16), а не 1024 на всех уровнях.
5
+
6
+ 512², латент f8 (64×36 = 2304 токена):
7
+ L0 ÷1 2304 tok C=192 conv-блоки (depthwise 3×3 + expand 4×) 6 enc / 4 dec
8
+ L1 ÷2 576 tok C=512 трансформер + cross-attn к тексту 4 enc / 3 dec
9
+ L2 ÷4 144 tok C=1024 трансформер, полное внимание 3 enc / 2 dec
10
+ L3 ÷8 36 tok C=1536 трансформер, полное внимание 2 (боттлнек)
11
+
12
+ Скипы — аддитивные (SD-стиль): после e0/e1/e2 признаки прибавляются в декодере.
13
+ Переходы: mean-pool 2× + Linear вниз, Linear + nearest 2× вверх.
14
+ Оценка: ~160M параметров и ~70 GFLOP против ~580 GFLOP у плоского 1024-стека.
15
+
16
+ Хвост — декодер AE (рендер латента в пиксели), текст — Ettin + SliceMixer (из `text.py`).
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+
28
+ ROOT = Path(__file__).resolve().parent.parent
29
+ sys.path.insert(0, str(ROOT / "vae"))
30
+
31
+ from ae.model import AutoEncoder # noqa: E402
32
+
33
+ DEV = "cuda" if torch.cuda.is_available() else "cpu"
34
+ _ROPE_CACHE: dict = {}
35
+
36
+
37
+ def t_embed(t: torch.Tensor, dim: int = 256) -> torch.Tensor:
38
+ """Синусоидальный эмбеддинг времени (t в [0,1] умножается на 1000)."""
39
+ half = dim // 2
40
+ f = torch.exp(-math.log(10000.0) * torch.arange(half, device=t.device, dtype=torch.float32) / half)
41
+ a = (t.float()[:, None] * 1000.0) * f[None]
42
+ return torch.cat([a.sin(), a.cos()], -1)
43
+
44
+
45
+ def body_rope(q, k, gh, gw, base: float = 100.0):
46
+ """2D RoPE: 4 четверти head_dim, две крутятся по x, две по y. Фаза — в латентных пикселях."""
47
+ key = (gh, gw, q.shape[-1], q.device, base)
48
+ tabs = _ROPE_CACHE.get(key)
49
+ if tabs is None:
50
+ dq = q.shape[-1] // 4
51
+ fr = torch.exp(-math.log(base) * torch.arange(dq, device=q.device, dtype=torch.float32) / dq)
52
+ yy = torch.arange(gh, device=q.device).repeat_interleave(gw).float()
53
+ xx = torch.arange(gw, device=q.device).repeat(gh).float()
54
+ ax, ay = xx[:, None] * fr, yy[:, None] * fr
55
+ tabs = (ax.cos()[None, None], ax.sin()[None, None], ay.cos()[None, None], ay.sin()[None, None])
56
+ _ROPE_CACHE[key] = tabs
57
+ cx, sx, cy, sy = tabs
58
+ dq = q.shape[-1] // 4
59
+
60
+ def rot(t):
61
+ a, b, c, e = t[..., :dq], t[..., dq:2 * dq], t[..., 2 * dq:3 * dq], t[..., 3 * dq:]
62
+ return torch.cat([a * cx - b * sx, a * sx + b * cx, c * cy - e * sy, c * sy + e * cy], dim=-1)
63
+
64
+ return rot(q), rot(k)
65
+
66
+
67
+ class RMSNorm(nn.Module):
68
+ def __init__(self, d: int, eps: float = 1e-6):
69
+ super().__init__()
70
+ self.d, self.eps = d, eps
71
+ self.scale = nn.Parameter(torch.zeros(d))
72
+
73
+ def forward(self, x):
74
+ return F.rms_norm(x.float(), (self.d,), weight=(self.scale.float() + 1.0), eps=self.eps).to(x.dtype)
75
+
76
+
77
+ class ConvBlock(nn.Module):
78
+ """ConvNeXt-ish: RMSNorm → depthwise 3×3 → expand 4× → GELU → project, всё под adaLN-гейтом."""
79
+
80
+ def __init__(self, c: int, expand: int = 4, text_dim: int | None = None, heads: int = 6):
81
+ super().__init__()
82
+ self.c, self.hid, self.h, self.hd = c, c * expand, heads, c // heads
83
+ self.norm = RMSNorm(c)
84
+ self.norm_x = RMSNorm(c)
85
+ self.dw = nn.Conv2d(c, c, 3, padding=1, groups=c)
86
+ self.up = nn.Linear(c, self.hid)
87
+ self.down = nn.Linear(self.hid, c)
88
+ self.mod = nn.Parameter(torch.zeros(5, c)) # shift, scale, gate | shift_x, scale_x
89
+ self.xatt = text_dim is not None
90
+ if self.xatt: # текст на мелком уровне: не экономим
91
+ self.xq, self.xo = nn.Linear(c, c, bias=False), nn.Linear(c, c, bias=False)
92
+ nn.init.zeros_(self.down.weight); nn.init.zeros_(self.down.bias)
93
+
94
+ def forward(self, x, vec, gh, gw, text=None, text_mask=None):
95
+ B = x.shape[0]
96
+ shift, scale, gate, shift_x, scale_x = (self.mod + vec).unbind(1)
97
+ h = self.norm(x) * (1 + scale[:, None]) + shift[:, None]
98
+ h = self.dw(h.transpose(1, 2).reshape(B, self.c, gh, gw)).flatten(2).transpose(1, 2)
99
+ x = x + gate[:, None] * self.down(F.gelu(self.up(h)))
100
+ if self.xatt and text is not None:
101
+ N = x.shape[1]
102
+ qq = self.xq(self.norm_x(x) * (1 + scale_x[:, None]) + shift_x[:, None])
103
+ qq = qq.reshape(B, N, self.h, self.hd).transpose(1, 2)
104
+ kk, vv = text
105
+ mask = text_mask[:, None, None, :] if text_mask is not None else None
106
+ xa = F.scaled_dot_product_attention(qq, kk, vv, attn_mask=mask)
107
+ x = x + self.xo(xa.transpose(1, 2).reshape(B, N, self.c))
108
+ return x
109
+
110
+
111
+ class AttnBlock(nn.Module):
112
+ """Pre-norm трансформер: self-attention с 2D RoPE + (опц.) cross-attn к тексту + FFN."""
113
+
114
+ def __init__(self, c: int, heads: int, xatt_dim: int | None, mlp: float = 2.0):
115
+ super().__init__()
116
+ self.c, self.h, self.hd = c, heads, c // heads
117
+ self.n1, self.n2, self.n3 = RMSNorm(c), RMSNorm(c), RMSNorm(c)
118
+ self.qkv = nn.Linear(c, 3 * c, bias=False)
119
+ self.o = nn.Linear(c, c, bias=False)
120
+ self.up, self.down = nn.Linear(c, int(c * mlp)), nn.Linear(int(c * mlp), c)
121
+ self.mod = nn.Parameter(torch.zeros(6, c))
122
+ self.xatt = xatt_dim is not None
123
+ if self.xatt:
124
+ self.xq, self.xo = nn.Linear(c, c, bias=False), nn.Linear(c, c, bias=False)
125
+ nn.init.zeros_(self.down.weight); nn.init.zeros_(self.down.bias)
126
+
127
+ def forward(self, x, vec, gh, gw, text=None, text_mask=None):
128
+ B, N, C = x.shape
129
+ s1, g1, s2, g2, s3, g3 = (self.mod + vec).unbind(1)
130
+ h = self.n1(x) * (1 + s1[:, None])
131
+ q, k, v = self.qkv(h).reshape(B, N, 3, self.h, self.hd).permute(2, 0, 3, 1, 4)
132
+ q, k = body_rope(q, k, gh, gw)
133
+ a = F.scaled_dot_product_attention(q, k, v).transpose(1, 2).reshape(B, N, C)
134
+ x = x + g1[:, None] * self.o(a)
135
+ if self.xatt and text is not None:
136
+ qq = self.xq(self.n2(x)).reshape(B, N, self.h, self.hd).transpose(1, 2)
137
+ kk, vv = text
138
+ mask = text_mask[:, None, None, :] if text_mask is not None else None
139
+ xa = F.scaled_dot_product_attention(qq, kk, vv, attn_mask=mask)
140
+ x = x + g2[:, None] * self.xo(xa.transpose(1, 2).reshape(B, N, C))
141
+ h = self.n3(x) * (1 + s3[:, None])
142
+ return x + g3[:, None] * self.down(F.gelu(self.up(h)))
143
+
144
+
145
+ class Body(nn.Module):
146
+ """U-стек: ширина 192 → 512 → 1024 → 1536 с аддитивными скипами."""
147
+
148
+ def __init__(self, latent_ch: int = 32, text_dim: int = 768,
149
+ widths: tuple = (192, 512, 1024, 1536), enc: tuple = (6, 4, 3, 2),
150
+ dec: tuple = (4, 3, 2), xatt: bool = True):
151
+ super().__init__()
152
+ w0, w1, w2, w3 = widths
153
+ self.widths, self.enc, self.dec = widths, enc, dec
154
+ heads = {0: 6, 1: 8, 2: 8, 3: 8}
155
+ self.stem = nn.Linear(latent_ch, w0)
156
+ self.e0 = nn.ModuleList([ConvBlock(w0, text_dim=text_dim if xatt else None) for _ in range(enc[0])])
157
+ self.e0a = AttnBlock(w0, 6, text_dim if xatt else None)
158
+ self.d0a = AttnBlock(w0, 6, text_dim if xatt else None)
159
+ self.e1 = nn.ModuleList([AttnBlock(w1, heads[1], text_dim if xatt else None) for _ in range(enc[1])])
160
+ self.e2 = nn.ModuleList([AttnBlock(w2, heads[2], text_dim if xatt else None) for _ in range(enc[2])])
161
+ self.e3 = nn.ModuleList([AttnBlock(w3, heads[3], text_dim if xatt else None) for _ in range(enc[3])])
162
+ self.d2 = nn.ModuleList([AttnBlock(w2, heads[2], text_dim if xatt else None) for _ in range(dec[2])])
163
+ self.d1 = nn.ModuleList([AttnBlock(w1, heads[1], text_dim if xatt else None) for _ in range(dec[1])])
164
+ self.d0 = nn.ModuleList([ConvBlock(w0, text_dim=text_dim if xatt else None) for _ in range(dec[0])])
165
+ self.dn01, self.dn12, self.dn23 = nn.Linear(w0, w1), nn.Linear(w1, w2), nn.Linear(w2, w3)
166
+ self.up32, self.up21, self.up10 = nn.Linear(w3, w2), nn.Linear(w2, w1), nn.Linear(w1, w0)
167
+ self.tmlp = nn.Sequential(nn.Linear(256, 512), nn.SiLU(), nn.Linear(512, 512))
168
+ self.pmlp = nn.Linear(text_dim, 512)
169
+ self.ada = nn.ModuleDict({f"l{i}": nn.Sequential(nn.SiLU(), nn.Linear(512, (5 if i == 0 else 6) * w))
170
+ for i, w in enumerate(widths)})
171
+ self.ada["l0a"] = nn.Sequential(nn.SiLU(), nn.Linear(512, 6 * w0)) # для self-attn на L0
172
+ self.xk0 = nn.Linear(text_dim, 2 * w0)
173
+ self.xk1, self.xk2, self.xk3 = (nn.Linear(text_dim, 2 * w1), nn.Linear(text_dim, 2 * w2),
174
+ nn.Linear(text_dim, 2 * w3))
175
+ self.out_norm = RMSNorm(w0)
176
+ self.out = nn.Linear(w0, latent_ch)
177
+ nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias)
178
+
179
+ # ---- переходы
180
+ @staticmethod
181
+ def downsample(x, gh, gw, proj):
182
+ B = x.shape[0]
183
+ d = x.view(B, gh // 2, 2, gw // 2, 2, -1).mean(dim=(2, 4))
184
+ return proj(d.reshape(B, (gh // 2) * (gw // 2), -1))
185
+
186
+ @staticmethod
187
+ def upsample(x, gh, gw, proj):
188
+ """(B, gh*gw, C) -> (B, 4*gh*gw, C') в родительском row-major порядке."""
189
+ B = x.shape[0]
190
+ d = proj(x).view(B, gh, 1, gw, 1, -1).expand(B, gh, 2, gw, 2, proj.out_features)
191
+ return d.reshape(B, 4 * gh * gw, proj.out_features)
192
+
193
+ def _txt(self, proj, text, heads):
194
+ B, L, _ = text.shape
195
+ kk, vv = proj(text).chunk(2, dim=-1)
196
+ hd = kk.shape[-1] // heads
197
+ return (kk.reshape(B, L, heads, hd).transpose(1, 2), vv.reshape(B, L, heads, hd).transpose(1, 2))
198
+
199
+ def forward(self, z, t, text, text_mask, gh=None, gw=None):
200
+ """gh/gw принимаются для совместимости с интерфейсом UDM и игнорируются."""
201
+ B, C, gh0, gw0 = z.shape
202
+ cond = self.tmlp(t_embed(t, 256)) + self.pmlp(text.mean(1))
203
+ v = [self.ada[f"l{i}"](cond).view(B, 5 if i == 0 else 6, -1) for i in range(4)]
204
+ v0a = self.ada["l0a"](cond).view(B, 6, -1)
205
+ tk0 = self._txt(self.xk0, text, 6)
206
+ tk1 = self._txt(self.xk1, text, 8)
207
+ tk2 = self._txt(self.xk2, text, 8)
208
+ tk3 = self._txt(self.xk3, text, 8)
209
+
210
+ x = self.stem(z.flatten(2).transpose(1, 2))
211
+ for blk in self.e0:
212
+ x = blk(x, v[0], gh0, gw0, tk0, text_mask)
213
+ x = self.e0a(x, v0a, gh0, gw0, tk0, text_mask)
214
+ s0 = x
215
+ g1h, g1w = gh0 // 2, gw0 // 2
216
+ x = self.downsample(x, gh0, gw0, self.dn01)
217
+ for blk in self.e1:
218
+ x = blk(x, v[1], g1h, g1w, tk1, text_mask)
219
+ s1 = x
220
+ g2h, g2w = g1h // 2, g1w // 2
221
+ x = self.downsample(x, g1h, g1w, self.dn12)
222
+ for blk in self.e2:
223
+ x = blk(x, v[2], g2h, g2w, tk2, text_mask)
224
+ s2 = x
225
+ g3h, g3w = g2h // 2, g2w // 2
226
+ x = self.downsample(x, g2h, g2w, self.dn23)
227
+ for blk in self.e3:
228
+ x = blk(x, v[3], g3h, g3w, tk3, text_mask)
229
+ # декодер: скипы аддитивные
230
+ x = self.upsample(x, g3h, g3w, self.up32) + s2
231
+ for blk in self.d2:
232
+ x = blk(x, v[2], g2h, g2w, tk2, text_mask)
233
+ x = self.upsample(x, g2h, g2w, self.up21) + s1
234
+ for blk in self.d1:
235
+ x = blk(x, v[1], g1h, g1w, tk1, text_mask)
236
+ x = self.upsample(x, g1h, g1w, self.up10) + s0
237
+ x = self.d0a(x, v0a, gh0, gw0, tk0, text_mask)
238
+ for blk in self.d0:
239
+ x = blk(x, v[0], gh0, gw0, tk0, text_mask)
240
+ return self.out(self.out_norm(x)).transpose(1, 2).reshape(B, C, gh0, gw0)
241
+
242
+
243
+ class UDiT(nn.Module):
244
+ """Пирамидальное тело + хвост-декодер AE; интерфейс как у UDM."""
245
+
246
+ def __init__(self, ae: AutoEncoder, text_dim: int = 768, **kw):
247
+ super().__init__()
248
+ self.ae = ae
249
+ self.body = Body(latent_ch=ae.config.latent_channels, text_dim=text_dim, **kw)
250
+
251
+ def to_raw(self, z):
252
+ return z * self.ae.latent_std + self.ae.latent_mean
253
+
254
+ @torch.no_grad()
255
+ def sample(self, text, text_mask, gh, gw, steps=4, tail_steps=4, generator=None,
256
+ param="x0", cfg=1.0, null_text=None, null_mask=None, clip_norm=0.0):
257
+ dev = self.ae.device
258
+ z = torch.randn(1, self.ae.config.latent_channels, gh, gw, device=dev, generator=generator)
259
+ with torch.autocast(dev.type, torch.bfloat16, enabled=dev.type == "cuda"):
260
+ for i in range(steps):
261
+ t = torch.full((1,), 1.0 - i / steps, device=dev)
262
+ o = self.body(z, t, text, text_mask).float()
263
+ if cfg > 1.0 and null_text is not None:
264
+ ou = self.body(z, t, null_text, null_mask).float()
265
+ o = ou + cfg * (o - ou)
266
+ v = o if param == "vel" else (z - o) / t.clamp(min=1e-2)[:, None, None, None]
267
+ z = z - v / steps
268
+ if clip_norm:
269
+ z = z.clamp(-clip_norm, clip_norm)
270
+ return self.ae.decode_raw(self.to_raw(z), steps=tail_steps, generator=generator)
src/publish.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Сборка публикационного дерева: код + доки + веса в fp16.
3
+
4
+ python3 src/publish.py --out tmp/publish # только собрать
5
+ python3 src/publish.py --out tmp/publish --push # собрать и залить (REPO_ID)
6
+
7
+ Локальные fp32-веса остаются на месте: они нужны обучению, а на Hub едет fp16.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import shutil
14
+ from pathlib import Path
15
+
16
+ import torch
17
+ from safetensors.torch import load_file, save_file
18
+
19
+ ROOT = Path(__file__).resolve().parent.parent
20
+ REPO_ID = "AiArtLab/ra"
21
+
22
+ COPY = ["README.md", ".gitattributes", ".gitignore", "docs", "src",
23
+ "vae/config.json", "vae/LICENSE", "vae/README.md", "vae/.gitattributes", "vae/example.py",
24
+ "vae/ae", "text_encoder/config.json", "text_encoder/tokenizer.json",
25
+ "text_encoder/tokenizer_config.json", "text_encoder/special_tokens_map.json",
26
+ "text_encoder/README.md", "text_encoder/.gitattributes",
27
+ "transformer/config.json"]
28
+
29
+
30
+ def half_file(src: Path, dst: Path, key_prefix: str = ""):
31
+ tensors = load_file(str(src))
32
+ out, overflow = {}, 0
33
+ for k, v in tensors.items():
34
+ name = f"{key_prefix}{k}" if key_prefix else k
35
+ if v.is_floating_point():
36
+ if v.numel() and float(v.abs().max()) > 65504:
37
+ overflow += 1
38
+ v = v.half()
39
+ out[name] = v.contiguous()
40
+ assert torch.isfinite(out[name]).all(), f"{name}: не-конечные значения после fp16"
41
+ dst.parent.mkdir(parents=True, exist_ok=True)
42
+ save_file(out, str(dst))
43
+ print(f"[fp16] {dst.relative_to(dst.parents[1])}: {len(out)} тензоров, "
44
+ f"{src.stat().st_size / 2**20:.0f} → {dst.stat().st_size / 2**20:.0f} МиБ"
45
+ + (f", ВНИМАНИЕ: {overflow} тензоров вне диапазона fp16" if overflow else ""), flush=True)
46
+
47
+
48
+ def main():
49
+ ap = argparse.ArgumentParser()
50
+ ap.add_argument("--out", default=str(ROOT / "tmp" / "publish"))
51
+ ap.add_argument("--repo", default=REPO_ID)
52
+ ap.add_argument("--push", action="store_true", help="залить собранное на Hub (публичный репозиторий)")
53
+ args = ap.parse_args()
54
+ out = Path(args.out)
55
+ if out.exists():
56
+ shutil.rmtree(out)
57
+ out.mkdir(parents=True)
58
+
59
+ for rel in COPY:
60
+ src = ROOT / rel
61
+ dst = out / rel
62
+ dst.parent.mkdir(parents=True, exist_ok=True)
63
+ if src.is_dir():
64
+ shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", ".ipynb_checkpoints"))
65
+ else:
66
+ shutil.copy2(src, dst)
67
+
68
+ half_file(ROOT / "transformer" / "diffusion_pytorch_model.safetensors",
69
+ out / "transformer" / "diffusion_pytorch_model.safetensors")
70
+ half_file(ROOT / "vae" / "model.safetensors", out / "vae" / "model.safetensors")
71
+
72
+ sd = torch.load(ROOT / "text_encoder" / "pytorch_model.bin", map_location="cpu", weights_only=True)
73
+ sd = {k: v for k, v in sd.items() if not k.startswith(("head.", "decoder."))} # MLM-голова не нужна
74
+ save_file({k: v.half().contiguous() for k, v in sd.items()},
75
+ str(out / "text_encoder" / "model.safetensors"))
76
+ print(f"[fp16] text_encoder/model.safetensors: {len(sd)} тензоров "
77
+ f"({sum(v.numel() for v in sd.values()) / 1e6:.1f}M), "
78
+ f"{(out / 'text_encoder' / 'model.safetensors').stat().st_size / 2**20:.0f} МиБ", flush=True)
79
+
80
+ cfg_path = out / "transformer" / "config.json"
81
+ cfg = json.loads(cfg_path.read_text())
82
+ cfg["storage_dtype"] = "float16"
83
+ cfg_path.write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
84
+
85
+ total = sum(f.stat().st_size for f in out.rglob("*") if f.is_file())
86
+ print(f"[готово] {out}: {total / 2**30:.2f} ГиБ", flush=True)
87
+
88
+ if args.push:
89
+ from huggingface_hub import HfApi
90
+ api = HfApi()
91
+ api.create_repo(args.repo, repo_type="model", private=False, exist_ok=True)
92
+ api.upload_folder(folder_path=str(out), repo_id=args.repo, repo_type="model",
93
+ commit_message=f"Ra fp16 (step {cfg['step']})")
94
+ print(f"[push] https://huggingface.co/{args.repo}", flush=True)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
src/sample.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Сэмпл из transformer/ (или из .pt чекпойнта обучения).
3
+
4
+ python3 src/sample.py --prompt "A photograph of a red fox in fresh snow." --out fox.png
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import torch
13
+ from PIL import Image
14
+
15
+ ROOT = Path(__file__).resolve().parent.parent
16
+ sys.path[:0] = [str(ROOT / "src"), str(ROOT / "vae")]
17
+
18
+ from model import DEV, UDiT # noqa: E402
19
+ from text import EttinText # noqa: E402
20
+ from ae.model import AutoEncoder # noqa: E402
21
+ from export import load_exported # noqa: E402
22
+
23
+
24
+ def main():
25
+ ap = argparse.ArgumentParser()
26
+ ap.add_argument("--model", default=str(ROOT / "transformer"), help="папка transformer/ или .pt")
27
+ ap.add_argument("--prompt", required=True)
28
+ ap.add_argument("--out", default=str(ROOT / "tmp" / "sample.png"))
29
+ ap.add_argument("--size", type=int, default=256, help="сторона кадра, кратна 64")
30
+ ap.add_argument("--steps", type=int, default=16, help="шагов тела")
31
+ ap.add_argument("--tail-steps", type=int, default=8, help="шагов декодера (хвоста)")
32
+ ap.add_argument("--cfg", type=float, default=2.5)
33
+ ap.add_argument("--seed", type=int, default=0)
34
+ ap.add_argument("--n", type=int, default=1, help="сколько картинок подряд от --seed")
35
+ ap.add_argument("--text-len", type=int, default=192)
36
+ args = ap.parse_args()
37
+
38
+ ae = AutoEncoder.from_pretrained(str(ROOT / "vae"), device=DEV)
39
+ enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len)
40
+ m = UDiT(ae, text_dim=enc.dim).to(DEV)
41
+
42
+ src = Path(args.model)
43
+ if src.is_dir():
44
+ step = load_exported(m, enc, ae, src, DEV).get("step")
45
+ else:
46
+ sd = torch.load(src, map_location=DEV, weights_only=False)
47
+ own = m.body.state_dict()
48
+ m.body.load_state_dict({k: v for k, v in sd["body"].items() if k in own}, strict=False)
49
+ enc.mixer.load_state_dict(sd["mixer"])
50
+ if sd.get("decoder") is not None:
51
+ ae.decoder.load_state_dict(sd["decoder"])
52
+ step = sd.get("step")
53
+ m.eval()
54
+ print(f"[model] {src}, шаг {step}", flush=True)
55
+
56
+ null_hs, null_mk = enc.slices([""], pad_to_len=args.text_len)
57
+ null_t = enc.mix(null_hs.float())
58
+ hs, mk = enc.slices([args.prompt], pad_to_len=args.text_len)
59
+ txt = enc.mix(hs.float())
60
+
61
+ out = Path(args.out)
62
+ out.parent.mkdir(parents=True, exist_ok=True)
63
+ gh = gw = args.size // 8
64
+ for i in range(args.n):
65
+ y = m.sample(txt, mk, gh, gw, steps=args.steps, tail_steps=args.tail_steps, cfg=args.cfg,
66
+ null_text=null_t, null_mask=null_mk,
67
+ generator=torch.Generator(DEV).manual_seed(args.seed + i))
68
+ im = Image.fromarray(((y[0].clamp(-1, 1) + 1) * 127.5).byte().permute(1, 2, 0).cpu().numpy())
69
+ path = out if args.n == 1 else out.with_name(f"{out.stem}_{args.seed + i}{out.suffix}")
70
+ im.save(path)
71
+ print(f"[ok] {path} {im.size[0]}x{im.size[1]}, seed {args.seed + i}", flush=True)
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
src/text.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Текстовый путь UDM: замороженный Ettin + обучаемый выбор слоёв (по идее moss/adapter).
2
+
3
+ Почему не один слой. Скрытые состояния растут по норме с глубиной (в Qwen3.5-9B — в 40 раз,
4
+ 13 у слоя 2 против 557 у слоя 27), поэтому «взять последний слой» теряет лексику и цифры,
5
+ а «взять ранний» — семантику. В moss/adapter это решили иначе: несколько срезов по K
6
+ нормируются ПОШТУЧНО, идут последовательностью длины K через 2 блока self-attention, затем
7
+ `Linear(K→1)` схлопывает ось срезов, а результат zero-init'ом добавляется к основному пути.
8
+ Тот же приём (плюс 0.8B вместо 9B) дал cos 0.924 к учителю.
9
+
10
+ Здесь то же самое, но цель — не регрессия к чужому пространству, а обусловливание ДиТ:
11
+ выход миксера идёт в `xkv` (кросс-внимание) и в `pmlp` (adaLN).
12
+
13
+ Срезы Ettin-150M: 22 слоя, global attention каждые 3 → берём выходы после глобальных слоёв.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import math
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+
23
+ # выходы после глобальных слоёв ModernBERT (22 слоя, global_attn_every_n_layers=3)
24
+ ETTIN_150M_SLICES = (2, 5, 8, 11, 14, 17, 20)
25
+
26
+
27
+ class AttnBlock(nn.Module):
28
+ """Pre-norm self-attention + MLP (для последовательности длины K — оси срезов)."""
29
+
30
+ def __init__(self, d: int, heads: int = 8, ffn_mult: float = 2.0):
31
+ super().__init__()
32
+ self.h = heads
33
+ self.n1, self.n2 = nn.LayerNorm(d), nn.LayerNorm(d)
34
+ self.qkv = nn.Linear(d, 3 * d)
35
+ self.proj = nn.Linear(d, d)
36
+ self.fc1 = nn.Linear(d, int(d * ffn_mult))
37
+ self.fc2 = nn.Linear(int(d * ffn_mult), d)
38
+
39
+ def forward(self, x):
40
+ b, n, d = x.shape
41
+ q, k, v = self.qkv(self.n1(x)).reshape(b, n, 3, self.h, d // self.h).permute(2, 0, 3, 1, 4)
42
+ a = F.scaled_dot_product_attention(q, k, v).transpose(1, 2).reshape(b, n, d)
43
+ x = x + self.proj(a)
44
+ return x + self.fc2(F.gelu(self.fc1(self.n2(x))))
45
+
46
+
47
+ class SliceMixer(nn.Module):
48
+ """Выбор слоя: пер-срезный LayerNorm → attention по оси срезов → Linear(K→1) → zero-init out.
49
+
50
+ blocks=0 оставляет только обученные веса слоёв (Linear(K→1) поверх нормированных срезов) —
51
+ дешёвый вариант «какой слой важнее», без внимания между срезами.
52
+ """
53
+
54
+ def __init__(self, n_slices: int, d: int, blocks: int = 2, heads: int = 8):
55
+ super().__init__()
56
+ self.n, self.d = n_slices, d
57
+ self.per = nn.LayerNorm(d, elementwise_affine=False)
58
+ self.blocks = nn.ModuleList([AttnBlock(d, heads) for _ in range(blocks)])
59
+ self.proj = nn.Linear(n_slices, 1, bias=False)
60
+ self.out = nn.Linear(d, d)
61
+ nn.init.zeros_(self.out.weight)
62
+ nn.init.zeros_(self.out.bias)
63
+
64
+ def forward(self, slices): # (B, L, K, D)
65
+ b, l, k, d = slices.shape
66
+ normed = self.per(slices) # пер-срезная нормировка ОБЯЗАТЕЛЬНА:
67
+ h = normed.reshape(b * l, k, d) # сырые нормы срезов растут с глубиной (52…548)
68
+ for blk in self.blocks:
69
+ h = blk(h)
70
+ mixed = self.out(self.proj(h.permute(0, 2, 1)).squeeze(-1)).reshape(b, l, d)
71
+ return normed.mean(2) + mixed # среднее по нормированным срезам + zero-init добавка
72
+
73
+ def layer_weights(self) -> torch.Tensor:
74
+ """Что микшер выучил: ненормированные веса слоёв (для логов)."""
75
+ return self.proj.weight.detach().squeeze()
76
+
77
+
78
+ class EttinText(nn.Module):
79
+ """Замороженный энкодер Ettin-150M + обучаемый SliceMixer. Возвращает (B,L,768) и маску."""
80
+
81
+ def __init__(self, path: str, device, max_len: int = 192, slices=ETTIN_150M_SLICES,
82
+ blocks: int = 2, dtype=torch.bfloat16):
83
+ super().__init__()
84
+ from transformers import AutoModel, AutoTokenizer
85
+ self.max_len, self.layer_idx = max_len, tuple(slices)
86
+ self.tok = AutoTokenizer.from_pretrained(path)
87
+ self.model = AutoModel.from_pretrained(path, dtype=dtype).to(device).eval().requires_grad_(False)
88
+ self.dim = self.model.config.hidden_size
89
+ self.mixer = SliceMixer(len(self.layer_idx), self.dim, blocks=blocks).to(device)
90
+ self.device = torch.device(device)
91
+
92
+ def tokenize(self, caps: list[str], pad_to: int = 8, pad_to_len: int | None = None):
93
+ # padding="longest" (а не до max_len): короткие капшены стоят дешевле
94
+ t = self.tok(caps, padding="longest", truncation=True, max_length=self.max_len,
95
+ return_tensors="pt")
96
+ ids, mask = t["input_ids"], t["attention_mask"].bool()
97
+ into = pad_to_len or ids.shape[1]
98
+ extra = into - ids.shape[1]
99
+ if extra > 0:
100
+ ids = F.pad(ids, (0, extra), value=self.tok.pad_token_id)
101
+ mask = F.pad(mask, (0, extra))
102
+ elif extra < 0:
103
+ ids, mask = ids[:, :into], mask[:, :into]
104
+ else:
105
+ extra = (-ids.shape[1]) % pad_to
106
+ if extra:
107
+ ids = F.pad(ids, (0, extra), value=self.tok.pad_token_id)
108
+ mask = F.pad(mask, (0, extra))
109
+ return ids.to(self.device), mask.to(self.device)
110
+
111
+ @torch.no_grad()
112
+ def slices(self, caps: list[str], pad_to_len: int | None = None):
113
+ """Срезы слоёв — считаются ОДИН раз на капшен (энкодер заморожен)."""
114
+ ids, mask = self.tokenize(caps, pad_to_len=pad_to_len)
115
+ out = self.model(input_ids=ids, attention_mask=mask, output_hidden_states=True)
116
+ return torch.stack([out.hidden_states[i] for i in self.layer_idx], dim=2), mask # (B,L,K,D)
117
+
118
+ def mix(self, hs):
119
+ """Обучаемый путь: среднее по нормированным срезам + zero-init добавка микшера."""
120
+ return self.mixer(hs.float())
121
+
122
+ def forward(self, caps: list[str]):
123
+ hs, mask = self.slices(caps)
124
+ return self.mix(hs), mask, hs
125
+
126
+ def trainable(self):
127
+ return list(self.mixer.parameters())
128
+
129
+
130
+ def empty_text(enc: EttinText):
131
+ """Пустой промпт = [CLS][SEP] — безусловный контекст для CFG."""
132
+ return enc([""])
src/train.py ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Обучение UDiT на папке «картинка + .txt». Квадратные кадры (--size), кэш латентов и текста.
3
+
4
+ python3 src/train.py --data ~/dataset/butterflies_256sq/train \
5
+ --eval ~/dataset/butterflies_256sq/eval --epochs 80 --batch 8
6
+
7
+ Что внутри (перенесено из привычек agat/train.py):
8
+ * бюджет в эпохах (`--epochs`) или шагах (`--steps`), lr: прогрев → cosine → полка;
9
+ * AdamW8bit (bitsandbytes), если он есть, иначе обычный AdamW — печатается, что взято;
10
+ * дропаут подписи (`--drop-text`) для CFG и дропаут слов (`--drop-word`) внутри подписи;
11
+ * сэмплы на старте (шаг 0), каждые `--sample-every`; держим `--keep` последних чекпойнтов и
12
+ `--samples-keep` сеток; сохранение атомарное;
13
+ * `--probe`: подбирает максимальный батч, который влезает в память, и печатает его;
14
+ * EMA сглаживание печатаемого лосса (`--ema-decay`), `--seed` случайный по умолчанию (печатается);
15
+ * метрики: mse_lat (общий, low t<0.3, high t>0.7), mse_pix, switch, tail(z0), hf(gen)/hf(AE), skip;
16
+ * SIGUSR1 печатает стеки всех потоков (видно, ждёт процесс GPU или сеть, без py-spy).
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import faulthandler
22
+ import json
23
+ import math
24
+ import os
25
+ import random
26
+ import shutil
27
+ import signal
28
+ import time
29
+ from pathlib import Path
30
+
31
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
32
+ signal.signal(signal.SIGINT, signal.default_int_handler)
33
+ faulthandler.register(signal.SIGUSR1)
34
+
35
+ import torch # noqa: E402
36
+ import torch.nn.functional as F # noqa: E402
37
+
38
+ from model import DEV, UDiT # noqa: E402
39
+ from text import EttinText # noqa: E402
40
+ from ae.model import AutoEncoder, patchify # noqa: E402
41
+
42
+ ROOT = Path(__file__).resolve().parent.parent
43
+
44
+
45
+ # ------------------------------------------------------------------ расписание
46
+ def lr_at(step: int, base: float, min_lr: float, warmup: int, period: int, start: int = 0) -> float:
47
+ """Прогрев `warmup` шагов от `start`, затем cosine до `min_lr` за `period` шагов."""
48
+ rel = step - start
49
+ if warmup and rel <= warmup:
50
+ return base * min(1.0, rel / max(warmup, 1))
51
+ if period <= warmup:
52
+ return base
53
+ p = min(1.0, (rel - warmup) / (period - warmup))
54
+ return min_lr + (base - min_lr) * 0.5 * (1 + math.cos(math.pi * p))
55
+
56
+
57
+ class HFImageStream:
58
+ """Поток parquet-шардов HF: декод/ресайз/кроп в фоновых потоках, обучение не ждёт диск.
59
+
60
+ Латент и текст НЕ кэшируются (каждая картинка встречается один раз), поэтому шаг дороже,
61
+ чем на папке: +3-4 мс на картинку у AE и +1.2 мс у Ettin.
62
+ """
63
+
64
+ def __init__(self, dataset, parts, size, fields, lo, hi, shuffle_buffer=1000, prefetch=48,
65
+ workers=8, seed=0):
66
+ import queue
67
+ self.q, self.stop = queue.Queue(maxsize=prefetch), False
68
+ self.size, self.fields, self.lo, self.hi = size, fields, lo, hi
69
+ self.dataset, self.parts = dataset, parts
70
+ self.buffer, self.workers, self.seed = shuffle_buffer, workers, seed
71
+ import threading
72
+ threading.Thread(target=self._loop, daemon=True).start()
73
+
74
+ def _items(self):
75
+ """Шарды всех частей перемешиваются между собой (иначе эпоха идёт блоками по частям)."""
76
+ import random
77
+ from datasets import load_dataset
78
+ from huggingface_hub import list_repo_files
79
+ repo = list_repo_files(self.dataset, repo_type="dataset")
80
+ files = [f for f in repo
81
+ if f.endswith(".parquet") and any(f.startswith(p + "/") for p in self.parts)]
82
+ if not files: # запасной путь, если листинг не отдал файлы
83
+ files = [f"hf://datasets/{self.dataset}/{p}/*.parquet" for p in self.parts]
84
+ else:
85
+ random.Random(self.seed).shuffle(files)
86
+ files = [f"hf://datasets/{self.dataset}/{f}" for f in files]
87
+ print(f"[stream] шардов: {len(files)} из частей {self.parts}", flush=True)
88
+ ds = load_dataset("parquet", data_files=files, split="train", streaming=True)
89
+ return ds.shuffle(buffer_size=self.buffer, seed=self.seed)
90
+
91
+ def _prepare(self, item):
92
+ from PIL import Image
93
+ import numpy as np
94
+ im = item["image"]
95
+ if im.mode != "RGB":
96
+ im = im.convert("RGB")
97
+ w, h = im.size
98
+ sc = self.size / min(w, h)
99
+ if abs(sc - 1.0) > 1e-3:
100
+ im = im.resize((max(self.size, round(w * sc)), max(self.size, round(h * sc))), Image.LANCZOS)
101
+ w, h = im.size
102
+ left, top = (w - self.size) // 2, (h - self.size) // 2
103
+ im = im.crop((left, top, left + self.size, top + self.size))
104
+ arr = torch.from_numpy(np.asarray(im, dtype=np.float32)).permute(2, 0, 1) / 127.5 - 1.0
105
+ return arr, self._caption(item)
106
+
107
+ def _caption(self, item):
108
+ clean = lambda x: (x or "").replace("\n", " ").strip() # noqa: E731
109
+ for f in self.fields:
110
+ t = clean(item.get(f))
111
+ if self.lo <= len(t) <= self.hi:
112
+ return t
113
+ return clean(item.get("caption_detail"))
114
+
115
+ def _loop(self):
116
+ import collections
117
+ from concurrent.futures import ThreadPoolExecutor
118
+ with ThreadPoolExecutor(max_workers=self.workers) as pool:
119
+ while not self.stop:
120
+ try:
121
+ it = iter(self._items())
122
+ flying = collections.deque()
123
+ while not self.stop:
124
+ while len(flying) < self.workers:
125
+ flying.append(pool.submit(self._prepare, next(it)))
126
+ self.q.put(flying.popleft().result())
127
+ except Exception as e:
128
+ print(f"[stream] сбой ({type(e).__name__}: {e}), переподключение через 5 с", flush=True)
129
+ time.sleep(5)
130
+
131
+ def batch(self, n):
132
+ imgs, caps = [], []
133
+ while len(imgs) < n:
134
+ arr, cap = self.q.get()
135
+ imgs.append(arr); caps.append(cap)
136
+ return torch.stack(imgs), caps
137
+
138
+
139
+ def hf_energy(t: torch.Tensor) -> float:
140
+ k = torch.tensor([[0.0, 1.0, 0.0], [1.0, -4.0, 1.0], [0.0, 1.0, 0.0]], device=t.device)
141
+ return F.conv2d(t.float().mean(1, keepdim=True), k.view(1, 1, 3, 3), padding=1).std().item()
142
+
143
+
144
+ def psnr(a, b) -> float:
145
+ return 10 * math.log10(4 / max((a.clamp(-1, 1) - b).pow(2).mean().item(), 1e-12))
146
+
147
+
148
+ def rotate(path: Path, keep: int, pattern: str):
149
+ """Оставить `keep` последних файлов, совпадающих с pattern (по номеру шага в имени)."""
150
+ files = sorted(path.glob(pattern))
151
+ for f in files[:-keep]:
152
+ f.unlink()
153
+
154
+
155
+ def main():
156
+ ap = argparse.ArgumentParser(description="UDiT: обучение на папке пар <имя>.jpg + <имя>.txt")
157
+ ap.add_argument("--data", default=str(Path.home() / "dataset" / "butterflies_256sq" / "train"))
158
+ ap.add_argument("--eval", default=str(Path.home() / "dataset" / "butterflies_256sq" / "eval"))
159
+ ap.add_argument("--limit", type=int, default=0, help="взять только первые N картинок (папка)")
160
+ ap.add_argument("--hf-dataset", default="", help="например LucasFang/FLUX-Reason-6M (поток)")
161
+ ap.add_argument("--parts", default="Aesthetics-Part01,Aesthetics-Part02,Imaginative,Text")
162
+ ap.add_argument("--caption-fields", default="caption_entity,caption_composition",
163
+ help="короткие поля подписи по порядку предпочтения (иначе caption_detail)")
164
+ ap.add_argument("--caption-len", default=(40, 400), nargs=2, type=int, metavar=("MIN", "MAX"))
165
+ ap.add_argument("--shuffle-buffer", type=int, default=1000)
166
+ ap.add_argument("--prefetch", type=int, default=48)
167
+ ap.add_argument("--workers", type=int, default=8)
168
+ ap.add_argument("--images-per-epoch", type=int, default=5_890_279,
169
+ help="сколько картинок в эпохе потока (для пересчёта --epochs в шаги)")
170
+ ap.add_argument("--size", type=int, default=256, help="разрешение обучения (кадры квадратные)")
171
+ ap.add_argument("--text-len", type=int, default=192, help="предел длины подписи в токенах")
172
+ ap.add_argument("--epochs", type=float, default=None, help="бюджет в эпохах (приоритетнее --steps)")
173
+ ap.add_argument("--steps", type=int, default=20000, help="бюджет в шагах оптимизатора")
174
+ ap.add_argument("--batch", type=int, default=8)
175
+ ap.add_argument("--accum", type=int, default=1, help="накопление: эффективный батч = batch*accum")
176
+ ap.add_argument("--probe", action="store_true", help="подобрать максимальный батч под память и выйти")
177
+ ap.add_argument("--lr", type=float, default=1e-4)
178
+ ap.add_argument("--lr-warmup", type=int, default=500)
179
+ ap.add_argument("--lr-min", type=float, default=None, help="нижняя точка cosine (по умолчанию 10%% от --lr)")
180
+ ap.add_argument("--lr-period", type=int, default=0, help="к какому шагу cosine доходит до --lr-min")
181
+ ap.add_argument("--drop-text", type=float, default=0.1, help="доля строк с пустой подписью (CFG)")
182
+ ap.add_argument("--drop-word", type=float, default=0.05, help="доля токенов подписи, скрытых в строке")
183
+ ap.add_argument("--cfg", type=float, default=1.5, help="CFG при показе сэмплов")
184
+ ap.add_argument("--sample-steps", type=int, default=8, help="шагов тела при сэмплинге")
185
+ ap.add_argument("--tail-steps", type=int, default=4, help="шагов декодера AE")
186
+ ap.add_argument("--lr-tail", type=float, default=3e-5)
187
+ ap.add_argument("--tail", choices=["frozen", "train"], default="train")
188
+ ap.add_argument("--init", default=None, help="стартовые веса: чекпойнт или папка transformer/")
189
+ ap.add_argument("--resume", action="store_true", help="продолжить с <out>/last.pt")
190
+ ap.add_argument("--out", default=str(ROOT / "tmp" / "run"))
191
+ ap.add_argument("--log-every", type=int, default=50)
192
+ ap.add_argument("--sample-every", type=int, default=1000, help="0 = не сэмплировать")
193
+ ap.add_argument("--save-every", type=int, default=2000, help="0 = только в конце")
194
+ ap.add_argument("--keep", type=int, default=2, help="сколько чекпойнтов держать")
195
+ ap.add_argument("--samples-keep", type=int, default=4, help="сколько сеток сэмплов держать")
196
+ ap.add_argument("--ema-decay", type=float, default=0.995, help="сглаживание печатаемого лосса")
197
+ ap.add_argument("--seed", type=int, default=None)
198
+ args = ap.parse_args()
199
+
200
+ seed = args.seed if args.seed is not None else random.SystemRandom().randrange(2 ** 31)
201
+ torch.manual_seed(seed)
202
+ print(f"[seed] {seed}", flush=True)
203
+
204
+ out = Path(args.out); (out / "samples").mkdir(parents=True, exist_ok=True)
205
+ (out / "ckpt").mkdir(exist_ok=True)
206
+ log_path = out / "log.csv"
207
+ if not (args.resume and log_path.exists()):
208
+ log_path.write_text("step,mse_lat,low,high,mse_pix,switch,bright,contrast,lat_pc_min,hf_gen,tail_z0,sec\n")
209
+
210
+ from PIL import Image
211
+ import numpy as np
212
+
213
+ def load(path: Path):
214
+ im = Image.open(path).convert("RGB")
215
+ return torch.from_numpy(np.asarray(im, dtype=np.float32)).permute(2, 0, 1) / 127.5 - 1
216
+
217
+ # ---------------------------------------------------------------- данные
218
+ stream = None
219
+ if args.hf_dataset:
220
+ stream = HFImageStream(args.hf_dataset, args.parts.split(","), args.size,
221
+ args.caption_fields.split(","), args.caption_len[0], args.caption_len[1],
222
+ args.shuffle_buffer, args.prefetch, args.workers, seed)
223
+ n = args.images_per_epoch
224
+ per_epoch = max(1, n // (args.batch * args.accum))
225
+ steps = int(round(args.epochs * per_epoch)) if args.epochs else args.steps
226
+ print(f"[data] поток {args.hf_dataset} [{args.parts}], ~{n} картинок/эпоха, "
227
+ f"{per_epoch} шагов/эпоха → бюджет {steps} шагов "
228
+ f"(~{steps * args.batch / 62 / 3600:.1f} ч при 62 картинках/с)", flush=True)
229
+ ae = AutoEncoder.from_pretrained(str(ROOT / "vae"), device=DEV)
230
+ enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len)
231
+ HS = MK = X = Z = None
232
+ HS_E, MK_E = enc.slices([""], pad_to_len=args.text_len)
233
+ HS_E = HS_E.to(torch.bfloat16)
234
+ else:
235
+ files = sorted(Path(args.data).glob("*.jpg")) + sorted(Path(args.data).glob("*.png"))
236
+ if args.limit:
237
+ files = files[: args.limit]
238
+ caps = [f.with_suffix(".txt").read_text().strip() for f in files]
239
+ ae = AutoEncoder.from_pretrained(str(ROOT / "vae"), device=DEV)
240
+ enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len)
241
+ X = torch.stack([load(f) for f in files]).to(DEV)
242
+ with torch.no_grad():
243
+ Z = torch.cat([ae.encode(X[i:i + 8]) for i in range(0, len(X), 8)])
244
+ HS, MK = enc.slices(caps)
245
+ HS, MK = HS.to(torch.bfloat16), MK
246
+ HS_E, MK_E = enc.slices([""], pad_to_len=HS.shape[1])
247
+ HS_E = HS_E.to(torch.bfloat16)
248
+ n = len(files)
249
+ per_epoch = max(1, n // (args.batch * args.accum))
250
+ steps = int(round(args.epochs * per_epoch)) if args.epochs else args.steps
251
+ print(f"[data] {n} пар, латент {tuple(Z.shape[1:])}, текст {tuple(HS.shape[1:])}, "
252
+ f"эпоха = {per_epoch} шагов → бюджет {steps}", flush=True)
253
+
254
+ # ---------------------------------------------------------------- модель и оптимизатор
255
+ m = UDiT(ae, text_dim=enc.dim).to(DEV)
256
+ start = 0
257
+
258
+ def load_body(sd):
259
+ own = m.body.state_dict()
260
+ take = {k: v for k, v in sd.items() if k in own and own[k].shape == v.shape}
261
+ m.body.load_state_dict(take, strict=False)
262
+ return len(take)
263
+
264
+ if args.init:
265
+ p = Path(args.init)
266
+ if p.is_dir():
267
+ from export import load_exported
268
+ cfg = load_exported(m, enc, ae, p, DEV)
269
+ print(f"[init] папка {p}, шаг {cfg.get('step')}", flush=True)
270
+ else:
271
+ sd = torch.load(p, map_location=DEV)
272
+ n_take = load_body(sd["body"]); enc.mixer.load_state_dict(sd["mixer"])
273
+ if sd.get("decoder") is not None and args.tail == "train":
274
+ ae.decoder.load_state_dict(sd["decoder"])
275
+ start = int(sd.get("step", 0))
276
+ print(f"[init] {p}: перенесено {n_take} тензоров, шаг {start}", flush=True)
277
+ if args.resume and (out / "last.pt").exists():
278
+ sd = torch.load(out / "last.pt", map_location=DEV)
279
+ load_body(sd["body"]); enc.mixer.load_state_dict(sd["mixer"])
280
+ if sd.get("decoder") is not None and args.tail == "train":
281
+ ae.decoder.load_state_dict(sd["decoder"])
282
+ start = int(sd.get("step", 0))
283
+ print(f"[resume] {out / 'last.pt'} → шаг {start}", flush=True)
284
+
285
+ n_body = sum(p.numel() for p in m.body.parameters())
286
+ n_tail = sum(p.numel() for p in ae.decoder.parameters())
287
+ print(f"[model] тело {n_body/1e6:.1f}M + миксер {sum(p.numel() for p in enc.trainable())/1e6:.2f}M "
288
+ f"+ хвост {n_tail/1e6:.1f}M [{args.tail}]", flush=True)
289
+
290
+ groups = [{"params": list(m.body.parameters()), "lr": args.lr},
291
+ {"params": enc.trainable(), "lr": args.lr}]
292
+ if args.tail == "train":
293
+ ae.decoder.requires_grad_(True)
294
+ groups.append({"params": list(ae.decoder.parameters()), "lr": args.lr_tail})
295
+ params = [p for g in groups for p in g["params"]]
296
+ try:
297
+ import bitsandbytes as bnb
298
+ opt = bnb.optim.AdamW8bit(groups, betas=(0.9, 0.95), weight_decay=0.0)
299
+ print("[opt] AdamW8bit", flush=True)
300
+ except Exception as e:
301
+ opt = torch.optim.AdamW(groups, betas=(0.9, 0.95), weight_decay=0.0)
302
+ print(f"[opt] AdamW fp32 (8bit недоступен: {type(e).__name__})", flush=True)
303
+
304
+ lr_min = args.lr_min if args.lr_min is not None else args.lr * 0.1
305
+ period = args.lr_period or steps # длина cosine в шагах, отсчёт от start
306
+ print(f"[lr] {args.lr:g}: прогрев {args.lr_warmup} от шага {start}, cosine до {lr_min:g} к шагу {start + period}", flush=True)
307
+
308
+ # ---------------------------------------------------------------- показ сэмплов
309
+ SAMPLE_PROMPTS = [
310
+ "A photograph of a red fox sleeping on a mossy log in a misty forest at dawn.",
311
+ "A city made of glass where rivers of light flow between the towers, cinematic lighting.",
312
+ 'A neon sign reading "hello world" mounted on a brick wall at night in the rain.',
313
+ "An impressionist oil painting of a woman with a parasol standing by a lake.",
314
+ ]
315
+ show_txt, show_x = [], []
316
+ if stream is not None:
317
+ for cap in SAMPLE_PROMPTS:
318
+ hs, mk = enc.slices([cap])
319
+ show_txt.append((enc.mix(hs.float()), mk))
320
+ else:
321
+ ev_all = sorted(Path(args.eval).glob("*.jpg")) + sorted(Path(args.eval).glob("*.png"))
322
+ ev_cls = {}
323
+ for f in ev_all:
324
+ ev_cls.setdefault(f.stem.split("_")[0], f)
325
+ show = [ev_cls[k] for k in list(ev_cls)[:4]] or ev_all[:4]
326
+ for f in show:
327
+ hs, mk = enc.slices([f.with_suffix(".txt").read_text().strip()], pad_to_len=HS.shape[1])
328
+ show_txt.append((enc.mix(hs.float()), mk))
329
+ show_x.append(load(f)[None].to(DEV))
330
+
331
+ def sample_grid(step):
332
+ tiles, hfs, ys = [], [], []
333
+ for txt, mk in show_txt:
334
+ y = m.sample(txt, mk, lat_hw[0], lat_hw[1], steps=args.sample_steps,
335
+ tail_steps=args.tail_steps, cfg=args.cfg,
336
+ null_text=enc.mix(HS_E.float()), null_mask=MK_E,
337
+ generator=torch.Generator(DEV).manual_seed(0))
338
+ tiles.append(Image.fromarray(((y[0].clamp(-1, 1) + 1) * 127.5).byte()
339
+ .permute(1, 2, 0).cpu().numpy()))
340
+ hfs.append(hf_energy(y)); ys.append(y)
341
+ if show_x:
342
+ ae_rec = ae.decode_raw(ae.encode(show_x[0]) * ae.latent_std + ae.latent_mean,
343
+ steps=args.tail_steps, generator=torch.Generator(DEV).manual_seed(0))
344
+ hf_ae = hf_energy(ae_rec)
345
+ else:
346
+ hf_ae = float("nan")
347
+ W, H = tiles[0].size
348
+ canvas = Image.new("RGB", (W * len(tiles), H), "white")
349
+ for i, t in enumerate(tiles):
350
+ canvas.paste(t, (i * W, 0))
351
+ canvas.save(out / "samples" / "latest.png")
352
+ canvas.save(out / "samples" / f"step_{step:07d}.png")
353
+ rotate(out / "samples", args.samples_keep, "step_*.png")
354
+ bright = float(torch.stack(ys).mean())
355
+ contrast = float(torch.stack(ys).std())
356
+ return hfs[0], hf_ae, canvas, bright, contrast
357
+
358
+ def save(step):
359
+ sd = {"body": {k: v.cpu() for k, v in m.body.state_dict().items()},
360
+ "mixer": {k: v.cpu() for k, v in enc.mixer.state_dict().items()},
361
+ "decoder": ({k: v.cpu() for k, v in ae.decoder.state_dict().items()}
362
+ if args.tail == "train" else None),
363
+ "step": step, "args": vars(args), "seed": seed}
364
+ torch.save(sd, out / "last.pt")
365
+ torch.save(sd, out / "ckpt" / f"step_{step:07d}.pt")
366
+ rotate(out / "ckpt", args.keep, "step_*.pt")
367
+
368
+ # ---------------------------------------------------------------- цикл
369
+ lat_hw = (args.size // 8, args.size // 8)
370
+ pkx = m.ae._packing([lat_hw] * args.batch)
371
+ if args.probe:
372
+ best = args.batch
373
+ for bs in (args.batch, args.batch * 2, args.batch * 4, args.batch * 8):
374
+ try:
375
+ z = torch.randn(bs, 32, lat_hw[0], lat_hw[1], device=DEV)
376
+ t = torch.rand(bs, device=DEV)
377
+ if stream is not None: # в потоке текста нет — берём фиктивный
378
+ hs_probe, mk_probe = enc.slices(["probe caption"] * bs)
379
+ else:
380
+ hs_probe, mk_probe = HS[:bs], MK[:bs]
381
+ with torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"):
382
+ m.body(z, t, enc.mix(hs_probe.float()), mk_probe).float().pow(2).mean().backward()
383
+ best = bs
384
+ m.zero_grad(set_to_none=True)
385
+ except torch.OutOfMemoryError:
386
+ m.zero_grad(set_to_none=True); torch.cuda.empty_cache(); break
387
+ print(f"[probe] максимальный батч ≈ {best} (пик {torch.cuda.max_memory_allocated()/1e9:.1f} ГБ)",
388
+ flush=True)
389
+ if stream is not None:
390
+ stream.stop = True
391
+ import os as _os
392
+ _os._exit(0)
393
+
394
+ if stream is not None:
395
+ def get_batch():
396
+ x, caps = stream.batch(args.batch)
397
+ x = x.to(DEV)
398
+ with torch.no_grad():
399
+ z = ae.encode(x)
400
+ hs, mk = enc.slices(caps)
401
+ return x, z, hs.to(torch.bfloat16), mk
402
+ else:
403
+ def get_batch():
404
+ idx = torch.randint(0, n, (args.batch,), device=DEV)
405
+ return X[idx], Z[idx], HS[idx], MK[idx]
406
+
407
+ hf_gen, hf_ae, _, bright0, contrast0 = sample_grid(start)
408
+ print(f"[sample] шаг {start}: яркость {bright0:+.3f}, контраст {contrast0:.3f}, hf {hf_gen:.3f}", flush=True)
409
+
410
+ t0 = time.perf_counter(); prev_el = t0
411
+ ema = {}
412
+ run = {"lat": 0.0, "low": 0.0, "high": 0.0, "pix": 0.0, "n_low": 0, "n_high": 0, "n": 0}
413
+ skips = 0
414
+ for step in range(start + 1, start + steps + 1):
415
+ opt.zero_grad(set_to_none=True)
416
+ ok = 0
417
+ for _ in range(args.accum):
418
+ x0, z0, hs, mk = get_batch()
419
+ # дропаут подписи (CFG) и дропаут слов внутри подписи
420
+ if args.drop_text:
421
+ drop = torch.rand(args.batch, device=DEV) < args.drop_text
422
+ if drop.any(): # пустая подпись обрезается под длину батча
423
+ L = hs.shape[1]
424
+ hs = torch.where(drop[:, None, None, None], HS_E[:, :L].expand_as(hs), hs)
425
+ mk = torch.where(drop[:, None], MK_E[:, :L].expand_as(mk), mk)
426
+ if args.drop_word:
427
+ word = (torch.rand(mk.shape, device=DEV) < args.drop_word) & mk
428
+ hs = hs.masked_fill(word[:, :, None, None], 0)
429
+ mk = mk & ~word
430
+ with torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"):
431
+ txt = enc.mix(hs.float())
432
+ tb = torch.rand(args.batch, device=DEV)
433
+ eb = torch.randn_like(z0)
434
+ zt = (1 - tb[:, None, None, None]) * z0 + tb[:, None, None, None] * eb
435
+ z_hat = m.body(zt, tb, txt, mk)
436
+ per_elem = (z_hat.float() - z0.float()).pow(2).mean(dim=(1, 2, 3))
437
+ low, high = tb < 0.3, tb > 0.7
438
+ loss_l = per_elem.mean()
439
+ raw = m.to_raw(z0 + (z_hat - z0).detach())
440
+ tx = torch.rand(args.batch, device=DEV)
441
+ ex = torch.randn_like(x0)
442
+ xt = (1 - tx[:, None, None, None]) * x0 + tx[:, None, None, None] * ex
443
+ xt_packed = pkx.pack([patchify(xt[i].to(ae.compute_dtype), 8) for i in range(args.batch)])
444
+ z_packed = pkx.pack([raw[i].flatten(1).transpose(0, 1) for i in range(args.batch)])
445
+ preds = ae.decoder(xt_packed, tx, z_packed, pkx)
446
+ pix_terms = torch.stack([(p.float() - x0[i]).pow(2).mean() for i, p in enumerate(preds)])
447
+ loss_p = pix_terms.mean()
448
+ total = loss_l + loss_p
449
+ if not torch.isfinite(total):
450
+ skips += 1
451
+ continue
452
+ (total / args.accum).backward()
453
+ ok += 1
454
+ run["lat"] += loss_l.item(); run["pix"] += loss_p.item()
455
+ if low.any():
456
+ run["low"] += per_elem[low].mean().item(); run["n_low"] += 1
457
+ if high.any():
458
+ run["high"] += per_elem[high].mean().item(); run["n_high"] += 1
459
+ if not ok:
460
+ print(f" ! шаг {step}: не-конечный лосс, пропуск (всего {skips})", flush=True)
461
+ continue
462
+ run["n"] += 1
463
+ torch.nn.utils.clip_grad_norm_(params, 1.0)
464
+ lr = lr_at(step, args.lr, lr_min, args.lr_warmup, period, start)
465
+ for g, base in zip(opt.param_groups, (args.lr, args.lr, args.lr_tail)):
466
+ g["lr"] = lr * (base / args.lr)
467
+ opt.step()
468
+
469
+ if step % args.log_every == 0:
470
+ d = args.ema_decay
471
+ cur = {"lat": run["lat"] / run["n"], "pix": run["pix"] / run["n"],
472
+ "low": run["low"] / max(run["n_low"], 1), "high": run["high"] / max(run["n_high"], 1)}
473
+ for k, v in cur.items():
474
+ ema[k] = v if k not in ema else d * ema[k] + (1 - d) * v
475
+ el = time.perf_counter() - t0; inc = el - prev_el; prev_el = el
476
+ print(f" {step:6d} | mse_lat {cur['lat']:.4f} (ema {ema['lat']:.4f}; low {cur['low']:.3f} "
477
+ f"high {cur['high']:.3f}) | mse_pix {cur['pix']:.4f} | lr {lr:.1e} | {el:6.0f}s "
478
+ f"({inc / max(run['n'], 1) * 1e3:.0f} мс/шаг) | skip {skips}", flush=True)
479
+ run = {"lat": 0.0, "low": 0.0, "high": 0.0, "pix": 0.0, "n_low": 0, "n_high": 0, "n": 0}
480
+
481
+ if args.sample_every and step % args.sample_every == 0:
482
+ zs = []
483
+ lat_pc_min = float("nan")
484
+ for txt, mk in show_txt[:2]:
485
+ z = torch.randn(1, 32, lat_hw[0], lat_hw[1], device=DEV,
486
+ generator=torch.Generator(DEV).manual_seed(7))
487
+ with torch.no_grad(), torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"):
488
+ for i in range(args.sample_steps):
489
+ t = torch.full((1,), 1.0 - i / args.sample_steps, device=DEV)
490
+ o = m.body(z, t, txt, mk).float()
491
+ ou = m.body(z, t, enc.mix(HS_E.float()), MK_E).float()
492
+ o = ou + args.cfg * (o - ou)
493
+ z = z - (z - o) / t.clamp(min=1e-2)[:, None, None, None] / args.sample_steps
494
+ zs.append(z)
495
+ switch = (((zs[0] - zs[1]).norm() / zs[0].norm()).item() if len(zs) > 1 else float("nan"))
496
+ if zs:
497
+ lat_pc_min = float(zs[0].std(dim=(0, 2, 3)).min()) # сторож сжатия дисперсии латента
498
+ hfg, hfa, canvas, bright, contrast = sample_grid(step)
499
+ if show_x:
500
+ z_ref = ae.encode(show_x[0])
501
+ tail_psnr = psnr(ae.decode_raw(m.to_raw(z_ref), steps=args.tail_steps,
502
+ generator=torch.Generator(DEV).manual_seed(0)), show_x[0])
503
+ else:
504
+ tail_psnr = float("nan")
505
+ el = time.perf_counter() - t0
506
+ print(f" ↳ switch {switch:.3f} | яркость {bright:+.3f} контраст {contrast:.3f} | "
507
+ f"латент: std {lat_pc_min:.2f} (у данных ~0.8–1.3) | hf {hfg:.3f} | tail(z0) {tail_psnr:.2f}",
508
+ flush=True)
509
+ with open(log_path, "a") as fh:
510
+ fh.write(f"{step},{ema.get('lat', 0):.5f},{ema.get('low', 0):.5f},{ema.get('high', 0):.5f},"
511
+ f"{ema.get('pix', 0):.5f},{switch:.4f},{bright:.4f},{contrast:.4f},"
512
+ f"{lat_pc_min:.4f},{hfg:.4f},{tail_psnr:.2f},{el:.1f}\n")
513
+ if args.save_every and step % args.save_every == 0:
514
+ save(step)
515
+ save(start + steps)
516
+ if stream is not None:
517
+ stream.stop = True # фоновые потоки не должны переживать интерпретатор
518
+ print("[done]", flush=True)
519
+ import os as _os
520
+ _os._exit(0) # обойти шумную финализацию streaming-генераторов
521
+
522
+
523
+ if __name__ == "__main__":
524
+ main()
text_encoder/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
text_encoder/README.md ADDED
@@ -0,0 +1,971 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ license: mit
5
+ pipeline_tags:
6
+ - fill-mask
7
+ - feature-extraction
8
+ library_name: transformers
9
+ tags:
10
+ - modernbert
11
+ - ettin
12
+ - encoder
13
+ - text-embeddings
14
+ - retrieval
15
+ - classification
16
+ ---
17
+
18
+ # Ettin: an Open Suite of Paired Encoders and Decoders
19
+
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
21
+ [![Paper](https://img.shields.io/badge/Paper-Arxiv-red)](https://arxiv.org/abs/2507.11412)
22
+ [![Models](https://img.shields.io/badge/🤗%20Hugging%20Face-12%20Models-blue)](https://huggingface.co/jhu-clsp)
23
+ [![Data](https://img.shields.io/badge/🤗%20Training%20Data-2T%20Tokens-green)](https://huggingface.co/datasets/jhu-clsp)
24
+ [![GitHub](https://img.shields.io/badge/GitHub-Code-black)](https://github.com/jhu-clsp/ettin-encoder-vs-decoder)
25
+
26
+ > 🎯 **TL;DR**: State-of-the-art paired encoder and decoder models (17M-1B params) trained identically for fair comparison with open data. Encoders beat ModernBERT. Decoders beat Llama 3.2/SmolLM2.
27
+
28
+ 📄 [Paper](https://arxiv.org/abs/2507.11412) | 🚀 [GitHub Repository](https://github.com/jhu-clsp/ettin-encoder-vs-decoder)
29
+
30
+ This model is part of the Ettin suite - the first collection of paired encoder-only and decoder-only models trained with identical data, architecture, and training recipes. Ettin enables fair comparisons between encoder and decoder architectures across multiple scales, providing state-of-the-art performance for open-data models in their respective size categories.
31
+
32
+ ## Table of Contents
33
+ - [Performance Highlights](#performance-highlights)
34
+ - [Quick Start](#quick-start)
35
+ - [Model Description](#model-description)
36
+ - [Training Data](#training-data)
37
+ - [Model Family](#model-family)
38
+ - [Encoder Models](#encoder-models)
39
+ - [Decoder Models](#decoder-models)
40
+ - [Cross-Objective Models](#cross-objective-models)
41
+ - [Accessing Training Checkpoints](#accessing-training-checkpoints)
42
+ - [Research Applications](#research-applications)
43
+ - [Training Details](#training-details)
44
+ - [Model Architecture](#model-architecture)
45
+ - [Usage Examples](#usage-examples)
46
+ - [Fine-tuning Examples](#fine-tuning-examples)
47
+ - [Citation](#citation)
48
+
49
+ ## 📊 Performance Highlights
50
+
51
+ ### Encoder Tasks (vs. ModernBERT)
52
+ - **GLUE Average**: 88.9 vs 88.4 (Base), 90.8 vs 90.4 (Large)
53
+ - **MTEB v2 English Retrieval**: 45.7 vs 43.9 (Base), 48.4 vs 47.0 (Large)
54
+ - **Code Search and Long Context**: Superior performance on CodeSearchNet and MLDR
55
+
56
+ ### Decoder Tasks (vs. SmolLM2 & Llama 3.2)
57
+ - **Average Score**: 46.2 vs 45.2 (SmolLM2-135M)
58
+ - **1B Model**: 59.0 vs 56.6 (Llama 3.2-1B)
59
+ - **Generative Tasks**: Competitive across all model sizes
60
+
61
+ ### Key Finding
62
+ **Architecture-specific advantages persist**: A 400M encoder outperforms a 1B decoder on classification tasks, while a 400M decoder outperforms a 1B encoder on generation tasks.
63
+
64
+ ## 🚀 Quick Start
65
+
66
+ ### Installation
67
+ ```bash
68
+ pip install torch>=1.9.0
69
+ # until the new pip release, install from main to use decoders (transformers>=4.54.X will contain it)
70
+ # encoders work with transformers>=4.48.0
71
+ pip install git+https://github.com/huggingface/transformers.git
72
+ ```
73
+
74
+ ### 30-Second Examples
75
+
76
+ **Encoder for Classification/Embeddings:**
77
+ ```python
78
+ from transformers import AutoTokenizer, AutoModel
79
+
80
+ tokenizer = AutoTokenizer.from_pretrained("jhu-clsp/ettin-encoder-150m")
81
+ model = AutoModel.from_pretrained("jhu-clsp/ettin-encoder-150m")
82
+ ```
83
+
84
+ **Decoder for Text Generation:**
85
+ ```python
86
+ from transformers import AutoTokenizer, AutoModelForCausalLM
87
+
88
+ tokenizer = AutoTokenizer.from_pretrained("jhu-clsp/ettin-decoder-150m")
89
+ model = AutoModelForCausalLM.from_pretrained("jhu-clsp/ettin-decoder-150m")
90
+ ```
91
+
92
+ ## Model Description
93
+
94
+ Ettin models are designed to provide a foundation for comparing encoder-only and decoder-only architectures. Unlike previous comparisons that were limited by different training data, architectures, and recipes, Ettin models use:
95
+
96
+ 1. **Identical training data** - Same high-quality mixture across all models
97
+ 2. **Open Training Data** - Data is available now with batch-level training data for each of the 250+ checkpoints
98
+ 3. **Matched architectures** - Only differing in attention patterns (bidirectional vs causal) and training objectives (MLM vs CLM)
99
+ 4. **Consistent training recipe** - Three-phase training with 2T tokens
100
+ 5. **Multiple scales** - From 17M to 1B parameters
101
+
102
+ This approach allows for true apples-to-apples comparisons between encoder and decoder models, revealing the inherent strengths of each architecture.
103
+
104
+ ## Training Data
105
+
106
+ The training data is publicly available and split across different phases:
107
+
108
+ - **Pre-training Data**: [jhu-clsp/ettin-pretraining-data](https://huggingface.co/datasets/jhu-clsp/ettin-pretraining-data) - 1.7T tokens of diverse data mixture
109
+ - **Mid-training/Extension Data**: [jhu-clsp/ettin-extension-data](https://huggingface.co/datasets/jhu-clsp/ettin-extension-data) - 250B tokens of higher-quality filtered data
110
+ - **Decay Phase Data**: [jhu-clsp/ettin-decay-data](https://huggingface.co/datasets/jhu-clsp/ettin-decay-data) - 100B tokens of premium data sources
111
+ - **Training Data Order**: [jhu-clsp/ettin-data-order](https://huggingface.co/datasets/jhu-clsp/ettin-data-order) - Batch-level training order (columns: input_ids, step)
112
+
113
+ ## Model Family
114
+
115
+ ### Encoder Models
116
+
117
+ | Size | Model | Parameters | Best For | Download |
118
+ |:-----|:------|:-----------|:---------|:---------|
119
+ | XXS | [ettin-encoder-17m](https://huggingface.co/jhu-clsp/ettin-encoder-17m) | 17M | Mobile/Edge devices | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-17m) |
120
+ | XS | [ettin-encoder-32m](https://huggingface.co/jhu-clsp/ettin-encoder-32m) | 32M | Fast inference | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-32m) |
121
+ | Small | [ettin-encoder-68m](https://huggingface.co/jhu-clsp/ettin-encoder-68m) | 68M | Balanced performance | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-68m) |
122
+ | Base | [ettin-encoder-150m](https://huggingface.co/jhu-clsp/ettin-encoder-150m) | 150M | Standard use cases | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-150m) |
123
+ | Large | [ettin-encoder-400m](https://huggingface.co/jhu-clsp/ettin-encoder-400m) | 400M | High accuracy needs | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-400m) |
124
+ | XL | [ettin-encoder-1b](https://huggingface.co/jhu-clsp/ettin-encoder-1b) | 1B | Best performance | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-1b) |
125
+
126
+ ### Decoder Models
127
+
128
+ | Size | Model | Parameters | Best For | Download |
129
+ |:-----|:------|:-----------|:---------|:---------|
130
+ | XXS | [ettin-decoder-17m](https://huggingface.co/jhu-clsp/ettin-decoder-17m) | 17M | Lightweight generation | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-17m) |
131
+ | XS | [ettin-decoder-32m](https://huggingface.co/jhu-clsp/ettin-decoder-32m) | 32M | Quick prototyping | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-32m) |
132
+ | Small | [ettin-decoder-68m](https://huggingface.co/jhu-clsp/ettin-decoder-68m) | 68M | Efficient generation | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-68m) |
133
+ | Base | [ettin-decoder-150m](https://huggingface.co/jhu-clsp/ettin-decoder-150m) | 150M | Standard generation | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-150m) |
134
+ | Large | [ettin-decoder-400m](https://huggingface.co/jhu-clsp/ettin-decoder-400m) | 400M | Quality generation | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-400m) |
135
+ | XL | [ettin-decoder-1b](https://huggingface.co/jhu-clsp/ettin-decoder-1b) | 1B | Best generation | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-1b) |
136
+
137
+ ### Cross-Objective Models
138
+
139
+ These models demonstrate what happens when you continue training encoders as decoders (and vice versa). **Important**: Load these models using the architecture they were *converted to*, not their original architecture.
140
+
141
+ #### Encoders Trained from Decoders (Decoder → MLM)
142
+ **Load as encoders** using `AutoModel` or `AutoModelForMaskedLM`:
143
+
144
+ | Size | Model | Parameters | Description | Download |
145
+ |:-----|:------|:-----------|:------------|:---------|
146
+ | XXS | [ettin-encoder-from-decoder-17m](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-17m) | 17M | Decoder → MLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-17m) |
147
+ | XS | [ettin-encoder-from-decoder-32m](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-32m) | 32M | Decoder → MLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-32m) |
148
+ | Small | [ettin-encoder-from-decoder-68m](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-68m) | 68M | Decoder → MLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-68m) |
149
+ | Base | [ettin-encoder-from-decoder-150m](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-150m) | 150M | Decoder → MLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-150m) |
150
+ | Large | [ettin-encoder-from-decoder-400m](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-400m) | 400M | Decoder → MLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-400m) |
151
+ | XL | [ettin-encoder-from-decoder-1b](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-1b) | 1B | Decoder → MLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-encoder-from-decoder-1b) |
152
+
153
+ #### Decoders Trained from Encoders (Encoder → CLM)
154
+ **Load as decoders** using `AutoModelForCausalLM`:
155
+
156
+ | Size | Model | Parameters | Description | Download |
157
+ |:-----|:------|:-----------|:------------|:---------|
158
+ | XXS | [ettin-decoder-from-encoder-17m](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-17m) | 17M | Encoder → CLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-17m) |
159
+ | XS | [ettin-decoder-from-encoder-32m](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-32m) | 32M | Encoder → CLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-32m) |
160
+ | Small | [ettin-decoder-from-encoder-68m](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-68m) | 68M | Encoder → CLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-68m) |
161
+ | Base | [ettin-decoder-from-encoder-150m](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-150m) | 150M | Encoder → CLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-150m) |
162
+ | Large | [ettin-decoder-from-encoder-400m](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-400m) | 400M | Encoder → CLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-400m) |
163
+ | XL | [ettin-decoder-from-encoder-1b](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-1b) | 1B | Encoder → CLM continued training | [![Download](https://img.shields.io/badge/🤗-Download-blue)](https://huggingface.co/jhu-clsp/ettin-decoder-from-encoder-1b) |
164
+
165
+ **Example Usage for Cross-Objective Models:**
166
+ ```python
167
+ # Encoder-from-decoder: Load as encoder
168
+ from transformers import AutoTokenizer, AutoModel
169
+ tokenizer = AutoTokenizer.from_pretrained("jhu-clsp/ettin-encoder-from-decoder-150m")
170
+ model = AutoModel.from_pretrained("jhu-clsp/ettin-encoder-from-decoder-150m")
171
+
172
+ # Decoder-from-encoder: Load as decoder
173
+ from transformers import AutoTokenizer, AutoModelForCausalLM
174
+ tokenizer = AutoTokenizer.from_pretrained("jhu-clsp/ettin-decoder-from-encoder-150m")
175
+ model = AutoModelForCausalLM.from_pretrained("jhu-clsp/ettin-decoder-from-encoder-150m")
176
+ ```
177
+
178
+ ## Accessing Training Checkpoints
179
+
180
+ Beyond the final models listed above, we provide access to intermediate training checkpoints for research and analysis purposes. These checkpoints allow you to study model behavior and performance throughout the training process. You can get the checkpoints either in HF format or raw for continued pre-training (e.g. Composer format).
181
+
182
+ #### Raw Checkpoints
183
+ All raw training checkpoints are available in the [jhu-clsp/ettin-checkpoints](https://huggingface.co/datasets/jhu-clsp/ettin-checkpoints) dataset.
184
+
185
+ #### HuggingFace Format Checkpoints
186
+ Each model repository contains multiple tagged versions representing different training stages:
187
+
188
+ - **`step{number}`** - Pretraining phase checkpoints (e.g., `step599525`, `step596528`)
189
+ - **`ext{number}`** - Extension/mid-training phase checkpoints (e.g., `ext1000`, `ext2000`)
190
+ - **`decay{number}`** - Decay phase checkpoints (e.g., `decay100`, `decay500`)
191
+
192
+ ```python
193
+ from transformers import AutoTokenizer, AutoModelForCausalLM
194
+
195
+ # Load a specific pretraining checkpoint
196
+ model = AutoModelForCausalLM.from_pretrained(
197
+ "jhu-clsp/ettin-decoder-400m",
198
+ revision="step590532" # Specific checkpoint tag
199
+ )
200
+
201
+ # Load an extension phase checkpoint
202
+ model = AutoModelForCausalLM.from_pretrained(
203
+ "jhu-clsp/ettin-decoder-400m",
204
+ revision="ext1000"
205
+ )
206
+
207
+ # Load a decay phase checkpoint
208
+ model = AutoModelForCausalLM.from_pretrained(
209
+ "jhu-clsp/ettin-decoder-400m",
210
+ revision="decay100"
211
+ )
212
+ ```
213
+
214
+ This checkpoint availability enables detailed analysis of training dynamics, loss curves, and capability emergence across the complete 2T token training process.
215
+
216
+
217
+ ## 🔬 Research Applications
218
+
219
+ ### What Makes Ettin Unique
220
+
221
+ Ettin provides the first **controlled comparison** of encoder vs. decoder architectures:
222
+
223
+ - **Identical Training Data**: Same 2T token mixture across all models
224
+ - **Matched Architectures**: Only attention patterns and objectives differ
225
+ - **Open Everything**: Training data, model weights, and batch-level training order
226
+ - **Multiple Scales**: Fair comparison from 17M to 1B parameters
227
+ - **250+ Checkpoints**: Complete training trajectory analysis
228
+
229
+ ### Use Cases for Researchers
230
+
231
+ - **Architecture Studies**: Compare encoder vs decoder capabilities fairly
232
+ - **Training Dynamics**: Analyze 250+ checkpoints with batch-level data ordering
233
+ - **Scaling Laws**: Study how architectural advantages change with scale
234
+ - **Transfer Learning**: Investigate cross-objective training effectiveness
235
+ - **Replication Studies**: First open replication of ModernBERT training recipe
236
+
237
+ ### Reproducibility
238
+
239
+ All training artifacts are publicly available:
240
+ - Training data with exact batch ordering
241
+ - Model checkpoints every 8.5B tokens
242
+ - Complete hyperparameter configurations
243
+ - Training code and evaluation scripts
244
+
245
+ ## Training Details
246
+
247
+ **Data:** High-quality mixture including DCLM, Dolma v1.7, scientific papers, code, and curated sources totaling 2T+ tokens
248
+
249
+ **Architecture:** Transformer with RoPE, GLU activations, and prenorm layers
250
+
251
+ **Training Phases:**
252
+ - **Pre-training**: 1.7T tokens with diverse data mixture
253
+ - **Mid-training**: 250B tokens with higher-quality filtered data and context extension to 8K
254
+ - **Decay phase**: 100B tokens with premium data sources
255
+
256
+ **Key Features:**
257
+ - Context length: Up to 8K tokens
258
+ - Vocabulary: 50,368 tokens (ModernBERT tokenizer)
259
+ - Deep but efficient architectures following MobileLLM principles
260
+
261
+ ## Model Architecture
262
+
263
+ | Parameter | 17M | 32M | 68M | 150M | 400M | 1B |
264
+ |:----------|:----|:----|:----|:-----|:-----|:---|
265
+ | Layers | 7 | 10 | 19 | 22 | 28 | 28 |
266
+ | Hidden Size | 256 | 384 | 512 | 768 | 1024 | 1792 |
267
+ | Intermediate Size | 384 | 576 | 768 | 1152 | 2624 | 3840 |
268
+ | Attention Heads | 4 | 6 | 8 | 12 | 16 | 28 |
269
+
270
+
271
+
272
+ ## Usage Examples
273
+
274
+ ### Encoder: Masked Language Modeling
275
+ <details>
276
+ <summary>Click to expand **encoder** usage examples</summary>
277
+
278
+ ```python
279
+ from transformers import AutoTokenizer, AutoModelForMaskedLM
280
+ import torch
281
+
282
+ # Load MLM model
283
+ tokenizer = AutoTokenizer.from_pretrained("jhu-clsp/ettin-encoder-150m")
284
+ model = AutoModelForMaskedLM.from_pretrained("jhu-clsp/ettin-encoder-150m")
285
+
286
+ def predict_masked_token(text):
287
+ inputs = tokenizer(text, return_tensors="pt")
288
+ with torch.no_grad():
289
+ outputs = model(**inputs)
290
+
291
+ # Get predictions for [MASK] tokens
292
+ mask_indices = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)
293
+ predictions = outputs.logits[mask_indices]
294
+
295
+ # Get top 5 predictions
296
+ top_tokens = torch.topk(predictions, 5, dim=-1)
297
+ return [tokenizer.decode(token) for token in top_tokens.indices[0]]
298
+
299
+ # Example
300
+ masked_text = "The capital of France is [MASK]."
301
+ predictions = predict_masked_token(masked_text)
302
+ print(f"Predictions: {predictions}")
303
+ ```
304
+
305
+ </details>
306
+
307
+ ### Decoder: Text Generation
308
+
309
+ <details>
310
+ <summary>Click to expand **decoder text generation**</summary>
311
+
312
+ ```python
313
+ from transformers import AutoTokenizer, AutoModelForCausalLM
314
+ import torch
315
+
316
+ # Load model and tokenizer
317
+ tokenizer = AutoTokenizer.from_pretrained("jhu-clsp/ettin-decoder-150m")
318
+ model = AutoModelForCausalLM.from_pretrained("jhu-clsp/ettin-decoder-150m")
319
+
320
+ # Set pad token if needed
321
+ if tokenizer.pad_token is None:
322
+ tokenizer.pad_token = tokenizer.eos_token
323
+
324
+ def generate_text(prompt, max_length=100, temperature=0.7):
325
+ inputs = tokenizer(prompt, return_tensors="pt")
326
+
327
+ with torch.no_grad():
328
+ outputs = model.generate(
329
+ inputs.input_ids,
330
+ max_length=max_length,
331
+ temperature=temperature,
332
+ do_sample=True,
333
+ pad_token_id=tokenizer.eos_token_id,
334
+ num_return_sequences=1
335
+ )
336
+
337
+ return tokenizer.decode(outputs[0], skip_special_tokens=True)
338
+
339
+ # Example usage
340
+ prompt = "The future of artificial intelligence is"
341
+ generated = generate_text(prompt)
342
+ print(generated)
343
+ ```
344
+
345
+ </details>
346
+
347
+
348
+ ## Fine-tuning Examples
349
+
350
+ ### Encoders
351
+ <details><summary>Click to see how to finetune this into a dense embedding model using Sentence Transformers</summary>
352
+
353
+ ```python
354
+ import argparse
355
+
356
+ from datasets import load_dataset
357
+ from sentence_transformers import (
358
+ SentenceTransformer,
359
+ SentenceTransformerTrainer,
360
+ SentenceTransformerTrainingArguments,
361
+ )
362
+ from sentence_transformers.evaluation import TripletEvaluator
363
+ from sentence_transformers.losses import CachedMultipleNegativesRankingLoss
364
+ from sentence_transformers.training_args import BatchSamplers
365
+
366
+ def main():
367
+ # parse the lr & model name
368
+ parser = argparse.ArgumentParser()
369
+ parser.add_argument("--lr", type=float, default=8e-5)
370
+ parser.add_argument("--model_name", type=str, default="jhu-clsp/ettin-encoder-150m")
371
+ args = parser.parse_args()
372
+ lr = args.lr
373
+ model_name = args.model_name
374
+ model_shortname = model_name.split("/")[-1]
375
+
376
+ # 1. Load a model to finetune
377
+ model = SentenceTransformer(model_name)
378
+
379
+ # 2. Load a dataset to finetune on
380
+ dataset = load_dataset(
381
+ "sentence-transformers/msmarco-co-condenser-margin-mse-sym-mnrl-mean-v1",
382
+ "triplet-hard",
383
+ split="train",
384
+ )
385
+ dataset_dict = dataset.train_test_split(test_size=1_000, seed=12)
386
+ train_dataset = dataset_dict["train"].select(range(1_250_000))
387
+ eval_dataset = dataset_dict["test"]
388
+
389
+ # 3. Define a loss function
390
+ loss = CachedMultipleNegativesRankingLoss(model, mini_batch_size=16) # Increase mini_batch_size if you have enough VRAM
391
+
392
+ run_name = f"{model_shortname}-DPR-{lr}"
393
+ # 4. (Optional) Specify training arguments
394
+ args = SentenceTransformerTrainingArguments(
395
+ # Required parameter:
396
+ output_dir=f"output/{model_shortname}/{run_name}",
397
+ # Optional training parameters:
398
+ num_train_epochs=1,
399
+ per_device_train_batch_size=512,
400
+ per_device_eval_batch_size=512,
401
+ warmup_ratio=0.05,
402
+ fp16=False, # Set to False if GPU can't handle FP16
403
+ bf16=True, # Set to True if GPU supports BF16
404
+ batch_sampler=BatchSamplers.NO_DUPLICATES, # (Cached)MultipleNegativesRankingLoss benefits from no duplicates
405
+ learning_rate=lr,
406
+ # Optional tracking/debugging parameters:
407
+ save_strategy="steps",
408
+ save_steps=500,
409
+ save_total_limit=2,
410
+ logging_steps=500,
411
+ run_name=run_name, # Used in `wandb`, `tensorboard`, `neptune`, etc. if installed
412
+ )
413
+
414
+ # 5. (Optional) Create an evaluator & evaluate the base model
415
+ dev_evaluator = TripletEvaluator(
416
+ anchors=eval_dataset["query"],
417
+ positives=eval_dataset["positive"],
418
+ negatives=eval_dataset["negative"],
419
+ name="msmarco-co-condenser-dev",
420
+ )
421
+ dev_evaluator(model)
422
+
423
+ # 6. Create a trainer & train
424
+ trainer = SentenceTransformerTrainer(
425
+ model=model,
426
+ args=args,
427
+ train_dataset=train_dataset,
428
+ eval_dataset=eval_dataset,
429
+ loss=loss,
430
+ evaluator=dev_evaluator,
431
+ )
432
+ trainer.train()
433
+
434
+ # 7. (Optional) Evaluate the trained model on the evaluator after training
435
+ dev_evaluator(model)
436
+
437
+ # 8. Save the model
438
+ model.save_pretrained(f"output/{model_shortname}/{run_name}/final")
439
+
440
+ # 9. (Optional) Push it to the Hugging Face Hub
441
+ model.push_to_hub(run_name, private=False)
442
+
443
+ if __name__ == "__main__":
444
+ main()
445
+ ```
446
+ </details>
447
+
448
+
449
+ <details><summary>Click to see how to finetune this into a multi-vector embedding model with PyLate</summary>
450
+
451
+ ```python
452
+ from datasets import load_dataset
453
+ from pylate import losses, models, utils
454
+ from sentence_transformers import (
455
+ SentenceTransformerTrainer,
456
+ SentenceTransformerTrainingArguments,
457
+ )
458
+
459
+ def main():
460
+ # Load the datasets required for knowledge distillation (train, queries, documents)
461
+ train = load_dataset(
462
+ path="lightonai/ms-marco-en-bge",
463
+ name="train",
464
+ )
465
+
466
+ queries = load_dataset(
467
+ path="lightonai/ms-marco-en-bge",
468
+ name="queries",
469
+ )
470
+
471
+ documents = load_dataset(
472
+ path="lightonai/ms-marco-en-bge",
473
+ name="documents",
474
+ )
475
+
476
+ # Set the transformation to load the documents/queries texts using the corresponding ids on the fly
477
+ train.set_transform(
478
+ utils.KDProcessing(queries=queries, documents=documents).transform,
479
+ )
480
+
481
+ # Define the base model, training parameters, and output directory
482
+ num_train_epochs = 1
483
+ lr = 8e-5
484
+ batch_size = 16
485
+ accum_steps = 1
486
+ model_name = "jhu-clsp/ettin-encoder-150m"
487
+ model_shortname = model_name.split("/")[-1]
488
+
489
+ # Set the run name for logging and output directory
490
+ run_name = f"{model_shortname}-colbert-KD-{lr}"
491
+ output_dir = f"output/{model_shortname}/{run_name}"
492
+
493
+ # Initialize the ColBERT model from the base model
494
+ model = models.ColBERT(model_name_or_path=model_name)
495
+
496
+ # Configure the training arguments (e.g., epochs, batch size, learning rate)
497
+ args = SentenceTransformerTrainingArguments(
498
+ output_dir=output_dir,
499
+ num_train_epochs=num_train_epochs,
500
+ per_device_train_batch_size=batch_size,
501
+ fp16=False, # Set to False if you get an error that your GPU can't run on FP16
502
+ bf16=True, # Set to True if you have a GPU that supports BF16
503
+ run_name=run_name,
504
+ logging_steps=10,
505
+ learning_rate=lr,
506
+ gradient_accumulation_steps=accum_steps,
507
+ warmup_ratio=0.05,
508
+ )
509
+
510
+ # Use the Distillation loss function for training
511
+ train_loss = losses.Distillation(model=model)
512
+
513
+ # Initialize the trainer
514
+ trainer = SentenceTransformerTrainer(
515
+ model=model,
516
+ args=args,
517
+ train_dataset=train,
518
+ loss=train_loss,
519
+ data_collator=utils.ColBERTCollator(tokenize_fn=model.tokenize),
520
+ )
521
+
522
+ # Start the training process
523
+ trainer.train()
524
+
525
+ model.save_pretrained(f"{output_dir}/final")
526
+
527
+ if __name__ == "__main__":
528
+ main()
529
+
530
+ ```
531
+ </details>
532
+
533
+ <details><summary>Click to see how to finetune this into a sparse retrieval model using Sentence Transformers</summary>
534
+
535
+ ```python
536
+ import logging
537
+
538
+ from datasets import load_dataset
539
+
540
+ from sentence_transformers import (
541
+ SparseEncoder,
542
+ SparseEncoderModelCardData,
543
+ SparseEncoderTrainer,
544
+ SparseEncoderTrainingArguments,
545
+ )
546
+ from sentence_transformers.sparse_encoder.evaluation import SparseNanoBEIREvaluator
547
+ from sentence_transformers.sparse_encoder.losses import SparseMultipleNegativesRankingLoss, SpladeLoss
548
+ from sentence_transformers.training_args import BatchSamplers
549
+
550
+ logging.basicConfig(format="%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO)
551
+
552
+ # 1. Load a model to finetune with 2. (Optional) model card data
553
+ model = SparseEncoder(
554
+ "jhu-clsp/ettin-encoder-150m",
555
+ model_card_data=SparseEncoderModelCardData(
556
+ language="en",
557
+ license="apache-2.0",
558
+ )
559
+ )
560
+
561
+ # 3. Load a dataset to finetune on
562
+ full_dataset = load_dataset("sentence-transformers/natural-questions", split="train").select(range(100_000))
563
+ dataset_dict = full_dataset.train_test_split(test_size=1_000, seed=12)
564
+ train_dataset = dataset_dict["train"]
565
+ eval_dataset = dataset_dict["test"]
566
+
567
+ # 4. Define a loss function
568
+ loss = SpladeLoss(
569
+ model=model,
570
+ loss=SparseMultipleNegativesRankingLoss(model=model),
571
+ query_regularizer_weight=5e-5,
572
+ document_regularizer_weight=3e-5,
573
+ )
574
+
575
+ # 5. (Optional) Specify training arguments
576
+ run_name = "splade-distilbert-base-uncased-nq"
577
+ args = SparseEncoderTrainingArguments(
578
+ # Required parameter:
579
+ output_dir=f"models/{run_name}",
580
+ # Optional training parameters:
581
+ num_train_epochs=1,
582
+ per_device_train_batch_size=16,
583
+ per_device_eval_batch_size=16,
584
+ learning_rate=2e-5,
585
+ warmup_ratio=0.1,
586
+ fp16=True, # Set to False if you get an error that your GPU can't run on FP16
587
+ bf16=False, # Set to True if you have a GPU that supports BF16
588
+ batch_sampler=BatchSamplers.NO_DUPLICATES, # MultipleNegativesRankingLoss benefits from no duplicate samples in a batch
589
+ # Optional tracking/debugging parameters:
590
+ eval_strategy="steps",
591
+ eval_steps=1000,
592
+ save_strategy="steps",
593
+ save_steps=1000,
594
+ save_total_limit=2,
595
+ logging_steps=200,
596
+ run_name=run_name, # Will be used in W&B if `wandb` is installed
597
+ )
598
+
599
+ # 6. (Optional) Create an evaluator & evaluate the base model
600
+ dev_evaluator = SparseNanoBEIREvaluator(dataset_names=["msmarco", "nfcorpus", "nq"], batch_size=16)
601
+
602
+ # 7. Create a trainer & train
603
+ trainer = SparseEncoderTrainer(
604
+ model=model,
605
+ args=args,
606
+ train_dataset=train_dataset,
607
+ eval_dataset=eval_dataset,
608
+ loss=loss,
609
+ evaluator=dev_evaluator,
610
+ )
611
+ trainer.train()
612
+
613
+ # 8. Evaluate the model performance again after training
614
+ dev_evaluator(model)
615
+
616
+ # 9. Save the trained model
617
+ model.save_pretrained(f"models/{run_name}/final")
618
+
619
+ # 10. (Optional) Push it to the Hugging Face Hub
620
+ model.push_to_hub(run_name)
621
+
622
+ ```
623
+ </details>
624
+
625
+ <details><summary>Click to see how to finetune this into a reranker model using Sentence Transformers</summary>
626
+
627
+ ```python
628
+ import logging
629
+ import traceback
630
+
631
+ import torch
632
+ from datasets import load_dataset
633
+
634
+ from sentence_transformers import SentenceTransformer
635
+ from sentence_transformers.cross_encoder import (
636
+ CrossEncoder,
637
+ CrossEncoderModelCardData,
638
+ CrossEncoderTrainer,
639
+ CrossEncoderTrainingArguments,
640
+ )
641
+ from sentence_transformers.cross_encoder.evaluation import (
642
+ CrossEncoderNanoBEIREvaluator,
643
+ CrossEncoderRerankingEvaluator,
644
+ )
645
+ from sentence_transformers.cross_encoder.losses import BinaryCrossEntropyLoss
646
+ from sentence_transformers.evaluation import SequentialEvaluator
647
+ from sentence_transformers.util import mine_hard_negatives
648
+
649
+ # Set the log level to INFO to get more information
650
+ logging.basicConfig(format="%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO)
651
+
652
+
653
+ def main():
654
+ model_name = "jhu-clsp/ettin-encoder-150m"
655
+
656
+ train_batch_size = 64
657
+ num_epochs = 1
658
+ num_hard_negatives = 5 # How many hard negatives should be mined for each question-answer pair
659
+
660
+ # 1a. Load a model to finetune with 1b. (Optional) model card data
661
+ model = CrossEncoder(
662
+ model_name,
663
+ model_card_data=CrossEncoderModelCardData(
664
+ language="en",
665
+ license="apache-2.0",
666
+ ),
667
+ )
668
+ print("Model max length:", model.max_length)
669
+ print("Model num labels:", model.num_labels)
670
+
671
+ # 2a. Load the GooAQ dataset: https://huggingface.co/datasets/sentence-transformers/gooaq
672
+ logging.info("Read the gooaq training dataset")
673
+ full_dataset = load_dataset("sentence-transformers/gooaq", split="train").select(range(100_000))
674
+ dataset_dict = full_dataset.train_test_split(test_size=1_000, seed=12)
675
+ train_dataset = dataset_dict["train"]
676
+ eval_dataset = dataset_dict["test"]
677
+ logging.info(train_dataset)
678
+ logging.info(eval_dataset)
679
+
680
+ # 2b. Modify our training dataset to include hard negatives using a very efficient embedding model
681
+ embedding_model = SentenceTransformer("sentence-transformers/static-retrieval-mrl-en-v1", device="cpu")
682
+ hard_train_dataset = mine_hard_negatives(
683
+ train_dataset,
684
+ embedding_model,
685
+ num_negatives=num_hard_negatives, # How many negatives per question-answer pair
686
+ margin=0, # Similarity between query and negative samples should be x lower than query-positive similarity
687
+ range_min=0, # Skip the x most similar samples
688
+ range_max=100, # Consider only the x most similar samples
689
+ sampling_strategy="top", # Sample the top negatives from the range
690
+ batch_size=4096, # Use a batch size of 4096 for the embedding model
691
+ output_format="labeled-pair", # The output format is (query, passage, label), as required by BinaryCrossEntropyLoss
692
+ use_faiss=True,
693
+ )
694
+ logging.info(hard_train_dataset)
695
+
696
+ # 2c. (Optionally) Save the hard training dataset to disk
697
+ # hard_train_dataset.save_to_disk("gooaq-hard-train")
698
+ # Load again with:
699
+ # hard_train_dataset = load_from_disk("gooaq-hard-train")
700
+
701
+ # 3. Define our training loss.
702
+ # pos_weight is recommended to be set as the ratio between positives to negatives, a.k.a. `num_hard_negatives`
703
+ loss = BinaryCrossEntropyLoss(model=model, pos_weight=torch.tensor(num_hard_negatives))
704
+
705
+ # 4a. Define evaluators. We use the CrossEncoderNanoBEIREvaluator, which is a light-weight evaluator for English reranking
706
+ nano_beir_evaluator = CrossEncoderNanoBEIREvaluator(
707
+ dataset_names=["msmarco", "nfcorpus", "nq"],
708
+ batch_size=train_batch_size,
709
+ )
710
+
711
+ # 4b. Define a reranking evaluator by mining hard negatives given query-answer pairs
712
+ # We include the positive answer in the list of negatives, so the evaluator can use the performance of the
713
+ # embedding model as a baseline.
714
+ hard_eval_dataset = mine_hard_negatives(
715
+ eval_dataset,
716
+ embedding_model,
717
+ corpus=full_dataset["answer"], # Use the full dataset as the corpus
718
+ num_negatives=30, # How many documents to rerank
719
+ batch_size=4096,
720
+ include_positives=True,
721
+ output_format="n-tuple",
722
+ use_faiss=True,
723
+ )
724
+ logging.info(hard_eval_dataset)
725
+ reranking_evaluator = CrossEncoderRerankingEvaluator(
726
+ samples=[
727
+ {
728
+ "query": sample["question"],
729
+ "positive": [sample["answer"]],
730
+ "documents": [sample[column_name] for column_name in hard_eval_dataset.column_names[2:]],
731
+ }
732
+ for sample in hard_eval_dataset
733
+ ],
734
+ batch_size=train_batch_size,
735
+ name="gooaq-dev",
736
+ # Realistic setting: only rerank the positives that the retriever found
737
+ # Set to True to rerank *all* positives
738
+ always_rerank_positives=False,
739
+ )
740
+
741
+ # 4c. Combine the evaluators & run the base model on them
742
+ evaluator = SequentialEvaluator([reranking_evaluator, nano_beir_evaluator])
743
+ evaluator(model)
744
+
745
+ # 5. Define the training arguments
746
+ short_model_name = model_name if "/" not in model_name else model_name.split("/")[-1]
747
+ run_name = f"reranker-{short_model_name}-gooaq-bce"
748
+ args = CrossEncoderTrainingArguments(
749
+ # Required parameter:
750
+ output_dir=f"models/{run_name}",
751
+ # Optional training parameters:
752
+ num_train_epochs=num_epochs,
753
+ per_device_train_batch_size=train_batch_size,
754
+ per_device_eval_batch_size=train_batch_size,
755
+ learning_rate=2e-5,
756
+ warmup_ratio=0.1,
757
+ fp16=False, # Set to False if you get an error that your GPU can't run on FP16
758
+ bf16=True, # Set to True if you have a GPU that supports BF16
759
+ dataloader_num_workers=4,
760
+ load_best_model_at_end=True,
761
+ metric_for_best_model="eval_gooaq-dev_ndcg@10",
762
+ # Optional tracking/debugging parameters:
763
+ eval_strategy="steps",
764
+ eval_steps=1000,
765
+ save_strategy="steps",
766
+ save_steps=1000,
767
+ save_total_limit=2,
768
+ logging_steps=200,
769
+ logging_first_step=True,
770
+ run_name=run_name, # Will be used in W&B if `wandb` is installed
771
+ seed=12,
772
+ )
773
+
774
+ # 6. Create the trainer & start training
775
+ trainer = CrossEncoderTrainer(
776
+ model=model,
777
+ args=args,
778
+ train_dataset=hard_train_dataset,
779
+ loss=loss,
780
+ evaluator=evaluator,
781
+ )
782
+ trainer.train()
783
+
784
+ # 7. Evaluate the final model, useful to include these in the model card
785
+ evaluator(model)
786
+
787
+ # 8. Save the final model
788
+ final_output_dir = f"models/{run_name}/final"
789
+ model.save_pretrained(final_output_dir)
790
+
791
+ # 9. (Optional) save the model to the Hugging Face Hub!
792
+ # It is recommended to run `huggingface-cli login` to log into your Hugging Face account first
793
+ try:
794
+ model.push_to_hub(run_name)
795
+ except Exception:
796
+ logging.error(
797
+ f"Error uploading model to the Hugging Face Hub:
798
+ {traceback.format_exc()}To upload it manually, you can run "
799
+ f"`huggingface-cli login`, followed by loading the model using `model = CrossEncoder({final_output_dir!r})` "
800
+ f"and saving it using `model.push_to_hub('{run_name}')`."
801
+ )
802
+
803
+
804
+ if __name__ == "__main__":
805
+ main()
806
+
807
+ ```
808
+ </details>
809
+
810
+ ### Decoders
811
+
812
+ <details>
813
+ <summary>Click to expand decoder training code</summary>
814
+
815
+ # Full training
816
+ ```bash
817
+ python trl/scripts/sft.py \
818
+ --model_name_or_path jhu-clsp/ettin-decoder-17m \
819
+ --dataset_name trl-lib/Capybara \
820
+ --learning_rate 2.0e-5 \
821
+ --num_train_epochs 1 \
822
+ --packing \
823
+ --per_device_train_batch_size 2 \
824
+ --gradient_accumulation_steps 8 \
825
+ --gradient_checkpointing \
826
+ --eos_token '<|im_end|>' \
827
+ --eval_strategy steps \
828
+ --eval_steps 100 \
829
+ --output_dir ettin-decoder-17m \
830
+ --push_to_hub
831
+ ```
832
+
833
+ # LoRA
834
+ ```bash
835
+ python trl/scripts/sft.py \
836
+ --model_name_or_path jhu-clsp/ettin-decoder-17m \
837
+ --dataset_name trl-lib/Capybara \
838
+ --learning_rate 2.0e-4 \
839
+ --num_train_epochs 1 \
840
+ --packing \
841
+ --per_device_train_batch_size 2 \
842
+ --gradient_accumulation_steps 8 \
843
+ --gradient_checkpointing \
844
+ --eos_token '<|im_end|>' \
845
+ --eval_strategy steps \
846
+ --eval_steps 100 \
847
+ --use_peft \
848
+ --lora_r 32 \
849
+ --lora_alpha 16 \
850
+ --output_dir ettin-decoder-17m \
851
+ --push_to_hub
852
+ ```
853
+
854
+ with `sft.py`:
855
+ ```python
856
+ import argparse
857
+
858
+ from datasets import load_dataset
859
+ from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
860
+ from transformers.models.auto.modeling_auto import MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES
861
+
862
+ from trl import (
863
+ ModelConfig,
864
+ ScriptArguments,
865
+ SFTConfig,
866
+ SFTTrainer,
867
+ TrlParser,
868
+ clone_chat_template,
869
+ get_kbit_device_map,
870
+ get_peft_config,
871
+ get_quantization_config,
872
+ )
873
+
874
+
875
+ def main(script_args, training_args, model_args):
876
+ ################
877
+ # Model init kwargs & Tokenizer
878
+ ################
879
+ quantization_config = get_quantization_config(model_args)
880
+ model_kwargs = dict(
881
+ revision=model_args.model_revision,
882
+ trust_remote_code=model_args.trust_remote_code,
883
+ attn_implementation=model_args.attn_implementation,
884
+ torch_dtype=model_args.torch_dtype,
885
+ use_cache=False if training_args.gradient_checkpointing else True,
886
+ device_map=get_kbit_device_map() if quantization_config is not None else None,
887
+ quantization_config=quantization_config,
888
+ )
889
+
890
+ # Create model
891
+ config = AutoConfig.from_pretrained(model_args.model_name_or_path)
892
+ valid_image_text_architectures = MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES.values()
893
+
894
+ if config.architectures and any(arch in valid_image_text_architectures for arch in config.architectures):
895
+ from transformers import AutoModelForImageTextToText
896
+
897
+ model_kwargs.pop("use_cache", None) # Image models do not support cache
898
+ model = AutoModelForImageTextToText.from_pretrained(model_args.model_name_or_path, **model_kwargs)
899
+ else:
900
+ model = AutoModelForCausalLM.from_pretrained(model_args.model_name_or_path, **model_kwargs)
901
+
902
+ # Create tokenizer
903
+ tokenizer = AutoTokenizer.from_pretrained(
904
+ model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, use_fast=True
905
+ )
906
+
907
+ # Set default chat template if needed
908
+ if tokenizer.chat_template is None:
909
+ # TODO: source should be passed as an argument
910
+ model, tokenizer = clone_chat_template(model, tokenizer, "Qwen/Qwen3-0.6B")
911
+
912
+ ################
913
+ # Dataset
914
+ ################
915
+ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config)
916
+
917
+ ################
918
+ # Training
919
+ ################
920
+ trainer = SFTTrainer(
921
+ model=model,
922
+ args=training_args,
923
+ train_dataset=dataset[script_args.dataset_train_split],
924
+ eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None,
925
+ processing_class=tokenizer,
926
+ peft_config=get_peft_config(model_args),
927
+ )
928
+
929
+ trainer.train()
930
+
931
+ # Save and push to hub
932
+ trainer.save_model(training_args.output_dir)
933
+ if training_args.push_to_hub:
934
+ trainer.push_to_hub(dataset_name=script_args.dataset_name)
935
+
936
+
937
+ def make_parser(subparsers: argparse._SubParsersAction = None):
938
+ dataclass_types = (ScriptArguments, SFTConfig, ModelConfig)
939
+ if subparsers is not None:
940
+ parser = subparsers.add_parser("sft", help="Run the SFT training script", dataclass_types=dataclass_types)
941
+ else:
942
+ parser = TrlParser(dataclass_types)
943
+ return parser
944
+
945
+
946
+ if __name__ == "__main__":
947
+ parser = make_parser()
948
+ # When using the trl cli, this script may be run with additional arguments, corresponding accelerate arguments.
949
+ # To ensure that their parsing does not interfere with the script arguments, parse the arguments with
950
+ # `return_remaining_strings=True`, then ignore the remaining strings.
951
+ script_args, training_args, model_args, _ = parser.parse_args_and_config(return_remaining_strings=True)
952
+ main(script_args, training_args, model_args)
953
+
954
+ ```
955
+ </details>
956
+
957
+ ## Citation
958
+
959
+ If you use Ettin models in your research, please cite our work:
960
+
961
+ ```bibtex
962
+ @misc{weller2025seqvsseqopen,
963
+ title={Seq vs Seq: An Open Suite of Paired Encoders and Decoders},
964
+ author={Orion Weller and Kathryn Ricci and Marc Marone and Antoine Chaffin and Dawn Lawrie and Benjamin Van Durme},
965
+ year={2025},
966
+ eprint={2507.11412},
967
+ archivePrefix={arXiv},
968
+ primaryClass={cs.CL},
969
+ url={https://arxiv.org/abs/2507.11412},
970
+ }
971
+ ```
text_encoder/config.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "ettin-encoder-150m",
3
+ "architectures": [
4
+ "ModernBertForMaskedLM"
5
+ ],
6
+ "attention_bias": false,
7
+ "attention_dropout": 0.0,
8
+ "bos_token_id": 50281,
9
+ "classifier_activation": "gelu",
10
+ "classifier_bias": false,
11
+ "classifier_dropout": 0.0,
12
+ "classifier_pooling": "mean",
13
+ "cls_token_id": 50281,
14
+ "decoder_bias": true,
15
+ "deterministic_flash_attn": false,
16
+ "embedding_dropout": 0.0,
17
+ "eos_token_id": 50282,
18
+ "global_attn_every_n_layers": 3,
19
+ "global_rope_theta": 160000.0,
20
+ "gradient_checkpointing": false,
21
+ "hidden_activation": "gelu",
22
+ "hidden_size": 768,
23
+ "initializer_cutoff_factor": 2.0,
24
+ "initializer_range": 0.02,
25
+ "intermediate_size": 1152,
26
+ "layer_norm_eps": 1e-05,
27
+ "local_attention": 128,
28
+ "local_rope_theta": 160000.0,
29
+ "max_position_embeddings": 7999,
30
+ "mlp_bias": false,
31
+ "mlp_dropout": 0.0,
32
+ "model_type": "modernbert",
33
+ "norm_bias": false,
34
+ "norm_eps": 1e-05,
35
+ "num_attention_heads": 12,
36
+ "num_hidden_layers": 22,
37
+ "pad_token_id": 50283,
38
+ "position_embedding_type": "sans_pos",
39
+ "sep_token_id": 50282,
40
+ "tie_word_embeddings": true,
41
+ "torch_dtype": "float32",
42
+ "transformers_version": "4.47.0.dev0",
43
+ "vocab_size": 50368,
44
+ "is_causal": false,
45
+ "causal_mask": false
46
+ }
text_encoder/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f95f652c607616a076c9e05c82f00f7372ce332de764dd016d230be6ab0d5864
3
+ size 298042336
text_encoder/special_tokens_map.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cls_token": {
3
+ "content": "[CLS]",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "mask_token": {
10
+ "content": "[MASK]",
11
+ "lstrip": true,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "pad_token": {
17
+ "content": "[PAD]",
18
+ "lstrip": false,
19
+ "normalized": false,
20
+ "rstrip": false,
21
+ "single_word": false
22
+ },
23
+ "sep_token": {
24
+ "content": "[SEP]",
25
+ "lstrip": false,
26
+ "normalized": false,
27
+ "rstrip": false,
28
+ "single_word": false
29
+ },
30
+ "unk_token": {
31
+ "content": "[UNK]",
32
+ "lstrip": false,
33
+ "normalized": false,
34
+ "rstrip": false,
35
+ "single_word": false
36
+ }
37
+ }
text_encoder/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
text_encoder/tokenizer_config.json ADDED
@@ -0,0 +1,944 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "|||IP_ADDRESS|||",
5
+ "lstrip": false,
6
+ "normalized": true,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": false
10
+ },
11
+ "1": {
12
+ "content": "<|padding|>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "50254": {
20
+ "content": " ",
21
+ "lstrip": false,
22
+ "normalized": true,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": false
26
+ },
27
+ "50255": {
28
+ "content": " ",
29
+ "lstrip": false,
30
+ "normalized": true,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": false
34
+ },
35
+ "50256": {
36
+ "content": " ",
37
+ "lstrip": false,
38
+ "normalized": true,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": false
42
+ },
43
+ "50257": {
44
+ "content": " ",
45
+ "lstrip": false,
46
+ "normalized": true,
47
+ "rstrip": false,
48
+ "single_word": false,
49
+ "special": false
50
+ },
51
+ "50258": {
52
+ "content": " ",
53
+ "lstrip": false,
54
+ "normalized": true,
55
+ "rstrip": false,
56
+ "single_word": false,
57
+ "special": false
58
+ },
59
+ "50259": {
60
+ "content": " ",
61
+ "lstrip": false,
62
+ "normalized": true,
63
+ "rstrip": false,
64
+ "single_word": false,
65
+ "special": false
66
+ },
67
+ "50260": {
68
+ "content": " ",
69
+ "lstrip": false,
70
+ "normalized": true,
71
+ "rstrip": false,
72
+ "single_word": false,
73
+ "special": false
74
+ },
75
+ "50261": {
76
+ "content": " ",
77
+ "lstrip": false,
78
+ "normalized": true,
79
+ "rstrip": false,
80
+ "single_word": false,
81
+ "special": false
82
+ },
83
+ "50262": {
84
+ "content": " ",
85
+ "lstrip": false,
86
+ "normalized": true,
87
+ "rstrip": false,
88
+ "single_word": false,
89
+ "special": false
90
+ },
91
+ "50263": {
92
+ "content": " ",
93
+ "lstrip": false,
94
+ "normalized": true,
95
+ "rstrip": false,
96
+ "single_word": false,
97
+ "special": false
98
+ },
99
+ "50264": {
100
+ "content": " ",
101
+ "lstrip": false,
102
+ "normalized": true,
103
+ "rstrip": false,
104
+ "single_word": false,
105
+ "special": false
106
+ },
107
+ "50265": {
108
+ "content": " ",
109
+ "lstrip": false,
110
+ "normalized": true,
111
+ "rstrip": false,
112
+ "single_word": false,
113
+ "special": false
114
+ },
115
+ "50266": {
116
+ "content": " ",
117
+ "lstrip": false,
118
+ "normalized": true,
119
+ "rstrip": false,
120
+ "single_word": false,
121
+ "special": false
122
+ },
123
+ "50267": {
124
+ "content": " ",
125
+ "lstrip": false,
126
+ "normalized": true,
127
+ "rstrip": false,
128
+ "single_word": false,
129
+ "special": false
130
+ },
131
+ "50268": {
132
+ "content": " ",
133
+ "lstrip": false,
134
+ "normalized": true,
135
+ "rstrip": false,
136
+ "single_word": false,
137
+ "special": false
138
+ },
139
+ "50269": {
140
+ "content": " ",
141
+ "lstrip": false,
142
+ "normalized": true,
143
+ "rstrip": false,
144
+ "single_word": false,
145
+ "special": false
146
+ },
147
+ "50270": {
148
+ "content": " ",
149
+ "lstrip": false,
150
+ "normalized": true,
151
+ "rstrip": false,
152
+ "single_word": false,
153
+ "special": false
154
+ },
155
+ "50271": {
156
+ "content": " ",
157
+ "lstrip": false,
158
+ "normalized": true,
159
+ "rstrip": false,
160
+ "single_word": false,
161
+ "special": false
162
+ },
163
+ "50272": {
164
+ "content": " ",
165
+ "lstrip": false,
166
+ "normalized": true,
167
+ "rstrip": false,
168
+ "single_word": false,
169
+ "special": false
170
+ },
171
+ "50273": {
172
+ "content": " ",
173
+ "lstrip": false,
174
+ "normalized": true,
175
+ "rstrip": false,
176
+ "single_word": false,
177
+ "special": false
178
+ },
179
+ "50274": {
180
+ "content": " ",
181
+ "lstrip": false,
182
+ "normalized": true,
183
+ "rstrip": false,
184
+ "single_word": false,
185
+ "special": false
186
+ },
187
+ "50275": {
188
+ "content": " ",
189
+ "lstrip": false,
190
+ "normalized": true,
191
+ "rstrip": false,
192
+ "single_word": false,
193
+ "special": false
194
+ },
195
+ "50276": {
196
+ "content": " ",
197
+ "lstrip": false,
198
+ "normalized": true,
199
+ "rstrip": false,
200
+ "single_word": false,
201
+ "special": false
202
+ },
203
+ "50277": {
204
+ "content": "|||EMAIL_ADDRESS|||",
205
+ "lstrip": false,
206
+ "normalized": true,
207
+ "rstrip": false,
208
+ "single_word": false,
209
+ "special": false
210
+ },
211
+ "50278": {
212
+ "content": "|||PHONE_NUMBER|||",
213
+ "lstrip": false,
214
+ "normalized": true,
215
+ "rstrip": false,
216
+ "single_word": false,
217
+ "special": false
218
+ },
219
+ "50279": {
220
+ "content": "<|endoftext|>",
221
+ "lstrip": false,
222
+ "normalized": false,
223
+ "rstrip": false,
224
+ "single_word": false,
225
+ "special": true
226
+ },
227
+ "50280": {
228
+ "content": "[UNK]",
229
+ "lstrip": false,
230
+ "normalized": false,
231
+ "rstrip": false,
232
+ "single_word": false,
233
+ "special": true
234
+ },
235
+ "50281": {
236
+ "content": "[CLS]",
237
+ "lstrip": false,
238
+ "normalized": false,
239
+ "rstrip": false,
240
+ "single_word": false,
241
+ "special": true
242
+ },
243
+ "50282": {
244
+ "content": "[SEP]",
245
+ "lstrip": false,
246
+ "normalized": false,
247
+ "rstrip": false,
248
+ "single_word": false,
249
+ "special": true
250
+ },
251
+ "50283": {
252
+ "content": "[PAD]",
253
+ "lstrip": false,
254
+ "normalized": false,
255
+ "rstrip": false,
256
+ "single_word": false,
257
+ "special": true
258
+ },
259
+ "50284": {
260
+ "content": "[MASK]",
261
+ "lstrip": true,
262
+ "normalized": false,
263
+ "rstrip": false,
264
+ "single_word": false,
265
+ "special": true
266
+ },
267
+ "50285": {
268
+ "content": "[unused0]",
269
+ "lstrip": false,
270
+ "normalized": true,
271
+ "rstrip": false,
272
+ "single_word": false,
273
+ "special": false
274
+ },
275
+ "50286": {
276
+ "content": "[unused1]",
277
+ "lstrip": false,
278
+ "normalized": true,
279
+ "rstrip": false,
280
+ "single_word": false,
281
+ "special": false
282
+ },
283
+ "50287": {
284
+ "content": "[unused2]",
285
+ "lstrip": false,
286
+ "normalized": true,
287
+ "rstrip": false,
288
+ "single_word": false,
289
+ "special": false
290
+ },
291
+ "50288": {
292
+ "content": "[unused3]",
293
+ "lstrip": false,
294
+ "normalized": true,
295
+ "rstrip": false,
296
+ "single_word": false,
297
+ "special": false
298
+ },
299
+ "50289": {
300
+ "content": "[unused4]",
301
+ "lstrip": false,
302
+ "normalized": true,
303
+ "rstrip": false,
304
+ "single_word": false,
305
+ "special": false
306
+ },
307
+ "50290": {
308
+ "content": "[unused5]",
309
+ "lstrip": false,
310
+ "normalized": true,
311
+ "rstrip": false,
312
+ "single_word": false,
313
+ "special": false
314
+ },
315
+ "50291": {
316
+ "content": "[unused6]",
317
+ "lstrip": false,
318
+ "normalized": true,
319
+ "rstrip": false,
320
+ "single_word": false,
321
+ "special": false
322
+ },
323
+ "50292": {
324
+ "content": "[unused7]",
325
+ "lstrip": false,
326
+ "normalized": true,
327
+ "rstrip": false,
328
+ "single_word": false,
329
+ "special": false
330
+ },
331
+ "50293": {
332
+ "content": "[unused8]",
333
+ "lstrip": false,
334
+ "normalized": true,
335
+ "rstrip": false,
336
+ "single_word": false,
337
+ "special": false
338
+ },
339
+ "50294": {
340
+ "content": "[unused9]",
341
+ "lstrip": false,
342
+ "normalized": true,
343
+ "rstrip": false,
344
+ "single_word": false,
345
+ "special": false
346
+ },
347
+ "50295": {
348
+ "content": "[unused10]",
349
+ "lstrip": false,
350
+ "normalized": true,
351
+ "rstrip": false,
352
+ "single_word": false,
353
+ "special": false
354
+ },
355
+ "50296": {
356
+ "content": "[unused11]",
357
+ "lstrip": false,
358
+ "normalized": true,
359
+ "rstrip": false,
360
+ "single_word": false,
361
+ "special": false
362
+ },
363
+ "50297": {
364
+ "content": "[unused12]",
365
+ "lstrip": false,
366
+ "normalized": true,
367
+ "rstrip": false,
368
+ "single_word": false,
369
+ "special": false
370
+ },
371
+ "50298": {
372
+ "content": "[unused13]",
373
+ "lstrip": false,
374
+ "normalized": true,
375
+ "rstrip": false,
376
+ "single_word": false,
377
+ "special": false
378
+ },
379
+ "50299": {
380
+ "content": "[unused14]",
381
+ "lstrip": false,
382
+ "normalized": true,
383
+ "rstrip": false,
384
+ "single_word": false,
385
+ "special": false
386
+ },
387
+ "50300": {
388
+ "content": "[unused15]",
389
+ "lstrip": false,
390
+ "normalized": true,
391
+ "rstrip": false,
392
+ "single_word": false,
393
+ "special": false
394
+ },
395
+ "50301": {
396
+ "content": "[unused16]",
397
+ "lstrip": false,
398
+ "normalized": true,
399
+ "rstrip": false,
400
+ "single_word": false,
401
+ "special": false
402
+ },
403
+ "50302": {
404
+ "content": "[unused17]",
405
+ "lstrip": false,
406
+ "normalized": true,
407
+ "rstrip": false,
408
+ "single_word": false,
409
+ "special": false
410
+ },
411
+ "50303": {
412
+ "content": "[unused18]",
413
+ "lstrip": false,
414
+ "normalized": true,
415
+ "rstrip": false,
416
+ "single_word": false,
417
+ "special": false
418
+ },
419
+ "50304": {
420
+ "content": "[unused19]",
421
+ "lstrip": false,
422
+ "normalized": true,
423
+ "rstrip": false,
424
+ "single_word": false,
425
+ "special": false
426
+ },
427
+ "50305": {
428
+ "content": "[unused20]",
429
+ "lstrip": false,
430
+ "normalized": true,
431
+ "rstrip": false,
432
+ "single_word": false,
433
+ "special": false
434
+ },
435
+ "50306": {
436
+ "content": "[unused21]",
437
+ "lstrip": false,
438
+ "normalized": true,
439
+ "rstrip": false,
440
+ "single_word": false,
441
+ "special": false
442
+ },
443
+ "50307": {
444
+ "content": "[unused22]",
445
+ "lstrip": false,
446
+ "normalized": true,
447
+ "rstrip": false,
448
+ "single_word": false,
449
+ "special": false
450
+ },
451
+ "50308": {
452
+ "content": "[unused23]",
453
+ "lstrip": false,
454
+ "normalized": true,
455
+ "rstrip": false,
456
+ "single_word": false,
457
+ "special": false
458
+ },
459
+ "50309": {
460
+ "content": "[unused24]",
461
+ "lstrip": false,
462
+ "normalized": true,
463
+ "rstrip": false,
464
+ "single_word": false,
465
+ "special": false
466
+ },
467
+ "50310": {
468
+ "content": "[unused25]",
469
+ "lstrip": false,
470
+ "normalized": true,
471
+ "rstrip": false,
472
+ "single_word": false,
473
+ "special": false
474
+ },
475
+ "50311": {
476
+ "content": "[unused26]",
477
+ "lstrip": false,
478
+ "normalized": true,
479
+ "rstrip": false,
480
+ "single_word": false,
481
+ "special": false
482
+ },
483
+ "50312": {
484
+ "content": "[unused27]",
485
+ "lstrip": false,
486
+ "normalized": true,
487
+ "rstrip": false,
488
+ "single_word": false,
489
+ "special": false
490
+ },
491
+ "50313": {
492
+ "content": "[unused28]",
493
+ "lstrip": false,
494
+ "normalized": true,
495
+ "rstrip": false,
496
+ "single_word": false,
497
+ "special": false
498
+ },
499
+ "50314": {
500
+ "content": "[unused29]",
501
+ "lstrip": false,
502
+ "normalized": true,
503
+ "rstrip": false,
504
+ "single_word": false,
505
+ "special": false
506
+ },
507
+ "50315": {
508
+ "content": "[unused30]",
509
+ "lstrip": false,
510
+ "normalized": true,
511
+ "rstrip": false,
512
+ "single_word": false,
513
+ "special": false
514
+ },
515
+ "50316": {
516
+ "content": "[unused31]",
517
+ "lstrip": false,
518
+ "normalized": true,
519
+ "rstrip": false,
520
+ "single_word": false,
521
+ "special": false
522
+ },
523
+ "50317": {
524
+ "content": "[unused32]",
525
+ "lstrip": false,
526
+ "normalized": true,
527
+ "rstrip": false,
528
+ "single_word": false,
529
+ "special": false
530
+ },
531
+ "50318": {
532
+ "content": "[unused33]",
533
+ "lstrip": false,
534
+ "normalized": true,
535
+ "rstrip": false,
536
+ "single_word": false,
537
+ "special": false
538
+ },
539
+ "50319": {
540
+ "content": "[unused34]",
541
+ "lstrip": false,
542
+ "normalized": true,
543
+ "rstrip": false,
544
+ "single_word": false,
545
+ "special": false
546
+ },
547
+ "50320": {
548
+ "content": "[unused35]",
549
+ "lstrip": false,
550
+ "normalized": true,
551
+ "rstrip": false,
552
+ "single_word": false,
553
+ "special": false
554
+ },
555
+ "50321": {
556
+ "content": "[unused36]",
557
+ "lstrip": false,
558
+ "normalized": true,
559
+ "rstrip": false,
560
+ "single_word": false,
561
+ "special": false
562
+ },
563
+ "50322": {
564
+ "content": "[unused37]",
565
+ "lstrip": false,
566
+ "normalized": true,
567
+ "rstrip": false,
568
+ "single_word": false,
569
+ "special": false
570
+ },
571
+ "50323": {
572
+ "content": "[unused38]",
573
+ "lstrip": false,
574
+ "normalized": true,
575
+ "rstrip": false,
576
+ "single_word": false,
577
+ "special": false
578
+ },
579
+ "50324": {
580
+ "content": "[unused39]",
581
+ "lstrip": false,
582
+ "normalized": true,
583
+ "rstrip": false,
584
+ "single_word": false,
585
+ "special": false
586
+ },
587
+ "50325": {
588
+ "content": "[unused40]",
589
+ "lstrip": false,
590
+ "normalized": true,
591
+ "rstrip": false,
592
+ "single_word": false,
593
+ "special": false
594
+ },
595
+ "50326": {
596
+ "content": "[unused41]",
597
+ "lstrip": false,
598
+ "normalized": true,
599
+ "rstrip": false,
600
+ "single_word": false,
601
+ "special": false
602
+ },
603
+ "50327": {
604
+ "content": "[unused42]",
605
+ "lstrip": false,
606
+ "normalized": true,
607
+ "rstrip": false,
608
+ "single_word": false,
609
+ "special": false
610
+ },
611
+ "50328": {
612
+ "content": "[unused43]",
613
+ "lstrip": false,
614
+ "normalized": true,
615
+ "rstrip": false,
616
+ "single_word": false,
617
+ "special": false
618
+ },
619
+ "50329": {
620
+ "content": "[unused44]",
621
+ "lstrip": false,
622
+ "normalized": true,
623
+ "rstrip": false,
624
+ "single_word": false,
625
+ "special": false
626
+ },
627
+ "50330": {
628
+ "content": "[unused45]",
629
+ "lstrip": false,
630
+ "normalized": true,
631
+ "rstrip": false,
632
+ "single_word": false,
633
+ "special": false
634
+ },
635
+ "50331": {
636
+ "content": "[unused46]",
637
+ "lstrip": false,
638
+ "normalized": true,
639
+ "rstrip": false,
640
+ "single_word": false,
641
+ "special": false
642
+ },
643
+ "50332": {
644
+ "content": "[unused47]",
645
+ "lstrip": false,
646
+ "normalized": true,
647
+ "rstrip": false,
648
+ "single_word": false,
649
+ "special": false
650
+ },
651
+ "50333": {
652
+ "content": "[unused48]",
653
+ "lstrip": false,
654
+ "normalized": true,
655
+ "rstrip": false,
656
+ "single_word": false,
657
+ "special": false
658
+ },
659
+ "50334": {
660
+ "content": "[unused49]",
661
+ "lstrip": false,
662
+ "normalized": true,
663
+ "rstrip": false,
664
+ "single_word": false,
665
+ "special": false
666
+ },
667
+ "50335": {
668
+ "content": "[unused50]",
669
+ "lstrip": false,
670
+ "normalized": true,
671
+ "rstrip": false,
672
+ "single_word": false,
673
+ "special": false
674
+ },
675
+ "50336": {
676
+ "content": "[unused51]",
677
+ "lstrip": false,
678
+ "normalized": true,
679
+ "rstrip": false,
680
+ "single_word": false,
681
+ "special": false
682
+ },
683
+ "50337": {
684
+ "content": "[unused52]",
685
+ "lstrip": false,
686
+ "normalized": true,
687
+ "rstrip": false,
688
+ "single_word": false,
689
+ "special": false
690
+ },
691
+ "50338": {
692
+ "content": "[unused53]",
693
+ "lstrip": false,
694
+ "normalized": true,
695
+ "rstrip": false,
696
+ "single_word": false,
697
+ "special": false
698
+ },
699
+ "50339": {
700
+ "content": "[unused54]",
701
+ "lstrip": false,
702
+ "normalized": true,
703
+ "rstrip": false,
704
+ "single_word": false,
705
+ "special": false
706
+ },
707
+ "50340": {
708
+ "content": "[unused55]",
709
+ "lstrip": false,
710
+ "normalized": true,
711
+ "rstrip": false,
712
+ "single_word": false,
713
+ "special": false
714
+ },
715
+ "50341": {
716
+ "content": "[unused56]",
717
+ "lstrip": false,
718
+ "normalized": true,
719
+ "rstrip": false,
720
+ "single_word": false,
721
+ "special": false
722
+ },
723
+ "50342": {
724
+ "content": "[unused57]",
725
+ "lstrip": false,
726
+ "normalized": true,
727
+ "rstrip": false,
728
+ "single_word": false,
729
+ "special": false
730
+ },
731
+ "50343": {
732
+ "content": "[unused58]",
733
+ "lstrip": false,
734
+ "normalized": true,
735
+ "rstrip": false,
736
+ "single_word": false,
737
+ "special": false
738
+ },
739
+ "50344": {
740
+ "content": "[unused59]",
741
+ "lstrip": false,
742
+ "normalized": true,
743
+ "rstrip": false,
744
+ "single_word": false,
745
+ "special": false
746
+ },
747
+ "50345": {
748
+ "content": "[unused60]",
749
+ "lstrip": false,
750
+ "normalized": true,
751
+ "rstrip": false,
752
+ "single_word": false,
753
+ "special": false
754
+ },
755
+ "50346": {
756
+ "content": "[unused61]",
757
+ "lstrip": false,
758
+ "normalized": true,
759
+ "rstrip": false,
760
+ "single_word": false,
761
+ "special": false
762
+ },
763
+ "50347": {
764
+ "content": "[unused62]",
765
+ "lstrip": false,
766
+ "normalized": true,
767
+ "rstrip": false,
768
+ "single_word": false,
769
+ "special": false
770
+ },
771
+ "50348": {
772
+ "content": "[unused63]",
773
+ "lstrip": false,
774
+ "normalized": true,
775
+ "rstrip": false,
776
+ "single_word": false,
777
+ "special": false
778
+ },
779
+ "50349": {
780
+ "content": "[unused64]",
781
+ "lstrip": false,
782
+ "normalized": true,
783
+ "rstrip": false,
784
+ "single_word": false,
785
+ "special": false
786
+ },
787
+ "50350": {
788
+ "content": "[unused65]",
789
+ "lstrip": false,
790
+ "normalized": true,
791
+ "rstrip": false,
792
+ "single_word": false,
793
+ "special": false
794
+ },
795
+ "50351": {
796
+ "content": "[unused66]",
797
+ "lstrip": false,
798
+ "normalized": true,
799
+ "rstrip": false,
800
+ "single_word": false,
801
+ "special": false
802
+ },
803
+ "50352": {
804
+ "content": "[unused67]",
805
+ "lstrip": false,
806
+ "normalized": true,
807
+ "rstrip": false,
808
+ "single_word": false,
809
+ "special": false
810
+ },
811
+ "50353": {
812
+ "content": "[unused68]",
813
+ "lstrip": false,
814
+ "normalized": true,
815
+ "rstrip": false,
816
+ "single_word": false,
817
+ "special": false
818
+ },
819
+ "50354": {
820
+ "content": "[unused69]",
821
+ "lstrip": false,
822
+ "normalized": true,
823
+ "rstrip": false,
824
+ "single_word": false,
825
+ "special": false
826
+ },
827
+ "50355": {
828
+ "content": "[unused70]",
829
+ "lstrip": false,
830
+ "normalized": true,
831
+ "rstrip": false,
832
+ "single_word": false,
833
+ "special": false
834
+ },
835
+ "50356": {
836
+ "content": "[unused71]",
837
+ "lstrip": false,
838
+ "normalized": true,
839
+ "rstrip": false,
840
+ "single_word": false,
841
+ "special": false
842
+ },
843
+ "50357": {
844
+ "content": "[unused72]",
845
+ "lstrip": false,
846
+ "normalized": true,
847
+ "rstrip": false,
848
+ "single_word": false,
849
+ "special": false
850
+ },
851
+ "50358": {
852
+ "content": "[unused73]",
853
+ "lstrip": false,
854
+ "normalized": true,
855
+ "rstrip": false,
856
+ "single_word": false,
857
+ "special": false
858
+ },
859
+ "50359": {
860
+ "content": "[unused74]",
861
+ "lstrip": false,
862
+ "normalized": true,
863
+ "rstrip": false,
864
+ "single_word": false,
865
+ "special": false
866
+ },
867
+ "50360": {
868
+ "content": "[unused75]",
869
+ "lstrip": false,
870
+ "normalized": true,
871
+ "rstrip": false,
872
+ "single_word": false,
873
+ "special": false
874
+ },
875
+ "50361": {
876
+ "content": "[unused76]",
877
+ "lstrip": false,
878
+ "normalized": true,
879
+ "rstrip": false,
880
+ "single_word": false,
881
+ "special": false
882
+ },
883
+ "50362": {
884
+ "content": "[unused77]",
885
+ "lstrip": false,
886
+ "normalized": true,
887
+ "rstrip": false,
888
+ "single_word": false,
889
+ "special": false
890
+ },
891
+ "50363": {
892
+ "content": "[unused78]",
893
+ "lstrip": false,
894
+ "normalized": true,
895
+ "rstrip": false,
896
+ "single_word": false,
897
+ "special": false
898
+ },
899
+ "50364": {
900
+ "content": "[unused79]",
901
+ "lstrip": false,
902
+ "normalized": true,
903
+ "rstrip": false,
904
+ "single_word": false,
905
+ "special": false
906
+ },
907
+ "50365": {
908
+ "content": "[unused80]",
909
+ "lstrip": false,
910
+ "normalized": true,
911
+ "rstrip": false,
912
+ "single_word": false,
913
+ "special": false
914
+ },
915
+ "50366": {
916
+ "content": "[unused81]",
917
+ "lstrip": false,
918
+ "normalized": true,
919
+ "rstrip": false,
920
+ "single_word": false,
921
+ "special": false
922
+ },
923
+ "50367": {
924
+ "content": "[unused82]",
925
+ "lstrip": false,
926
+ "normalized": true,
927
+ "rstrip": false,
928
+ "single_word": false,
929
+ "special": false
930
+ }
931
+ },
932
+ "clean_up_tokenization_spaces": true,
933
+ "cls_token": "[CLS]",
934
+ "mask_token": "[MASK]",
935
+ "model_max_length": 8192,
936
+ "pad_token": "[PAD]",
937
+ "sep_token": "[SEP]",
938
+ "tokenizer_class": "PreTrainedTokenizerFast",
939
+ "model_input_names": [
940
+ "input_ids",
941
+ "attention_mask"
942
+ ],
943
+ "unk_token": "[UNK]"
944
+ }
transformer/config.json ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "UDiT",
3
+ "name": "Ra",
4
+ "format": "udm/v1",
5
+ "step": 212000,
6
+ "latent": {
7
+ "channels": 32,
8
+ "spatial_factor": 8,
9
+ "mean": [
10
+ -4.137261,
11
+ 2.725146,
12
+ -1.435803,
13
+ -2.187023,
14
+ -4.161275,
15
+ -1.58918,
16
+ -2.527523,
17
+ 3.002302,
18
+ 2.793705,
19
+ -2.091501,
20
+ -2.632896,
21
+ -3.59589,
22
+ -3.069669,
23
+ -4.568713,
24
+ 3.26668,
25
+ -0.897648,
26
+ 2.072541,
27
+ 5.451158,
28
+ 1.747871,
29
+ -2.113284,
30
+ 2.03444,
31
+ -1.379274,
32
+ 0.66369,
33
+ 2.712183,
34
+ -0.111181,
35
+ 6.054838,
36
+ 1.235881,
37
+ -2.02777,
38
+ 3.663243,
39
+ -2.454632,
40
+ 1.141335,
41
+ 1.786136
42
+ ],
43
+ "std": [
44
+ 1.030738,
45
+ 0.951682,
46
+ 1.079687,
47
+ 0.957871,
48
+ 1.198266,
49
+ 1.102266,
50
+ 1.213605,
51
+ 0.947544,
52
+ 1.085991,
53
+ 1.093223,
54
+ 0.9027,
55
+ 1.010205,
56
+ 0.920466,
57
+ 1.044107,
58
+ 1.023179,
59
+ 0.992978,
60
+ 0.991627,
61
+ 1.073585,
62
+ 1.114629,
63
+ 1.056403,
64
+ 1.093012,
65
+ 1.015109,
66
+ 1.007069,
67
+ 0.976026,
68
+ 0.982324,
69
+ 1.093805,
70
+ 1.036086,
71
+ 1.005727,
72
+ 1.104078,
73
+ 1.042298,
74
+ 1.095522,
75
+ 0.960255
76
+ ],
77
+ "source": "vae/ (Muinez/f8c32-ae)"
78
+ },
79
+ "text": {
80
+ "encoder": "text_encoder/ (jhu-clsp/ettin-encoder-150m, 768-dim, 22 слоя)",
81
+ "max_len": 192,
82
+ "slices": [
83
+ 2,
84
+ 5,
85
+ 8,
86
+ 11,
87
+ 14,
88
+ 17,
89
+ 20
90
+ ],
91
+ "slice_blocks": 2,
92
+ "trainable": "text_mixer.*"
93
+ },
94
+ "body": {
95
+ "kind": "пирамидальный U: ширина растёт к низу",
96
+ "levels": [
97
+ {
98
+ "level": 0,
99
+ "tokens": "÷1",
100
+ "width": 192,
101
+ "blocks": "conv 6 enc / 4 dec + 1 self-attn на сторону",
102
+ "text": "кросс-внимание в каждом conv-блоке и в self-attn"
103
+ },
104
+ {
105
+ "level": 1,
106
+ "tokens": "÷2",
107
+ "width": 512,
108
+ "blocks": "attn 4 enc / 3 dec",
109
+ "text": "кросс-внимание"
110
+ },
111
+ {
112
+ "level": 2,
113
+ "tokens": "÷4",
114
+ "width": 1024,
115
+ "blocks": "attn 3 enc / 2 dec",
116
+ "text": "кросс-внимание"
117
+ },
118
+ {
119
+ "level": 3,
120
+ "tokens": "÷8",
121
+ "width": 1536,
122
+ "blocks": "attn 2 (боттлнек)",
123
+ "text": "кросс-внимание"
124
+ }
125
+ ],
126
+ "skips": "аддитивные (SD-стиль): e0/e1/e2 прибавляются в декодере",
127
+ "down": "mean-pool 2× + Linear",
128
+ "up": "Linear + nearest 2×",
129
+ "rope": "2D RoPE, base 100, фаза в латентных пикселях на каждом уровне",
130
+ "parametrization": "x0 латента"
131
+ },
132
+ "tail": {
133
+ "source": "декодер AE, 6 блоков @512, PixelShuffle ×3",
134
+ "trained_as": "flow по пикселям, условие z0 + (ошибка тела).detach()",
135
+ "sample_steps": 4
136
+ },
137
+ "params": {
138
+ "body": 143.33,
139
+ "text_mixer": 10.04,
140
+ "tail": 23.87,
141
+ "total": 177.24
142
+ },
143
+ "training": {
144
+ "dataset": "LucasFang/FLUX-Reason-6M",
145
+ "parts": "Aesthetics-Part01,Aesthetics-Part02,Imaginative,Text",
146
+ "size": 256,
147
+ "steps": 212000,
148
+ "batch": 16,
149
+ "accum": 1,
150
+ "lr": 0.0001,
151
+ "lr_tail": 3e-05,
152
+ "drop_text": 0.1,
153
+ "init": "tmp/flux2/ckpt/step_0192000.pt",
154
+ "metrics": "FLUX-Reason Part01 on top of the 20k butterfly run; continued on Parts 01+02+Imaginative+Text from step 192000, mse_lat 0.244 (ema 0.245), low 0.026, high 0.57, mse_pix 0.019"
155
+ },
156
+ "storage_dtype": "float16"
157
+ }
transformer/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f288233710ddf86c9d50099d59fa5d1f19438ff9a55b418bcc27ff2a23405658
3
+ size 354543188
vae/.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
vae/LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
vae/README.md ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ ---
4
+
5
+ # Image Autoencoder (8x, 32 channels, flow-matching decoder)
6
+
7
+ Inference-only package for an image autoencoder intended as a latent space for diffusion models.
8
+
9
+ - **Compression:** 8x spatially, 32 latent channels — an `H x W` RGB image becomes a `32 x H/8 x W/8` latent.
10
+ - **Encoder:** deterministic transformer with local window attention (2D RoPE), works at any resolution
11
+ whose sides are multiples of 8.
12
+ - **Decoder:** conditional flow-matching model (x0-prediction). It starts from Gaussian noise and is
13
+ integrated in `N` steps (default **4**) conditioned on the latent.
14
+ - **Normalization built in:** `encode()` returns latents already normalized per channel
15
+ (`(z - mean) / std`, roughly zero mean / unit variance); `decode()` undoes it. Use
16
+ `encode_raw()` / `decode_raw()` for unnormalized latents.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install torch safetensors huggingface_hub numpy pillow
22
+ # optional: triton (CUDA) for the fused window-attention kernel; otherwise PyTorch SDPA is used
23
+ ```
24
+
25
+ The model code lives in the `ae/` folder of this repo:
26
+
27
+ ```bash
28
+ huggingface-cli download Muinez/f8c32-ae --local-dir f8c32-ae
29
+ export PYTHONPATH=$PWD/f8c32-ae:$PYTHONPATH
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ import torch
36
+ from ae import AutoEncoder
37
+
38
+ model = AutoEncoder.from_pretrained("Muinez/f8c32-ae", device="cuda") # bf16 compute on CUDA
39
+
40
+ x = ... # (B, 3, H, W) float in [-1, 1], H and W multiples of 8
41
+ z = model.encode(x) # (B, 32, H/8, W/8), normalized latents
42
+ y = model.decode(z, steps=4) # (B, 3, H, W), approximately in [-1, 1]
43
+ y = y.clamp(-1, 1)
44
+
45
+ # reproducible decoding
46
+ g = torch.Generator("cuda").manual_seed(0)
47
+ y = model.decode(z, steps=4, generator=g)
48
+
49
+ # raw (unnormalized) latents
50
+ z_raw = model.encode_raw(x)
51
+ y = model.decode_raw(z_raw, steps=4)
52
+ ```
53
+
54
+ ### Batch of different resolutions / aspect ratios
55
+
56
+ `encode` / `decode` (and the `_raw` variants) also take a **list** of tensors of different
57
+ sizes and return a list. The whole list goes through the model in one forward pass: each
58
+ image keeps its own size (no resizing or cropping), is cut into its own grid of 8x8 patch
59
+ tokens, and the grids are packed into one sequence batch padded only up to the largest image
60
+ area. Attention never crosses into other images or padding, so results match encoding each
61
+ image on its own. Only the decoder's small convolutional upsampling head runs per group of
62
+ equal-shape images.
63
+
64
+ ```python
65
+ from PIL import Image
66
+ import numpy as np
67
+
68
+ def load(path, max_side=1024):
69
+ im = Image.open(path).convert("RGB")
70
+ s = min(1.0, max_side / max(im.size))
71
+ w, h = (int(im.width * s) // 8 * 8, int(im.height * s) // 8 * 8) # sides: multiples of 8
72
+ im = im.resize((w, h), Image.LANCZOS)
73
+ return torch.from_numpy(np.asarray(im)).permute(2, 0, 1) # uint8 (3, H, W)
74
+
75
+ images = [load("portrait.jpg"), load("landscape.png"), load("square.webp")] # any sizes
76
+ latents = model.encode(images) # list of (32, H_i/8, W_i/8), normalized
77
+ recons = model.decode(latents, steps=4) # list of (3, H_i, W_i)
78
+ ```
79
+
80
+ Images may be uint8 `[0, 255]` or float `[-1, 1]`.
81
+
82
+ See `example.py` for a complete round trip (`python example.py input.png recon.png`).
83
+
84
+ ### Notes
85
+
86
+ - `steps` trades speed for detail: 1 step is fastest and slightly softer, 4 is the default.
87
+ Because the decoder samples from noise, different seeds give slightly different fine detail.
88
+ - `dtype` in `from_pretrained` sets the compute dtype (default: bfloat16 on CUDA, float32 on CPU).
89
+ Encoder weights are cast to it; the decoder keeps float32 weights and runs under autocast.
90
+ - On CUDA with Triton installed, a fused window-attention kernel is used for any image sizes;
91
+ otherwise (CPU, no Triton) an equivalent masked SDPA path is used. Force SDPA with
92
+ `import ae.window_attention as wa; wa.BACKEND = "sdpa"`.
93
+
94
+ ## Files
95
+
96
+ - `config.json` — architecture and latent normalization statistics
97
+ - `model.safetensors` — encoder and decoder weights (float32)
98
+ - `ae/` — model code (`AutoEncoder`, `AEConfig`)
99
+ - `example.py` — round-trip example
100
+ - `LICENSE` — Apache License 2.0
101
+
102
+ ## License
103
+
104
+ Apache License 2.0 — see `LICENSE`.
vae/ae/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .model import AEConfig, AutoEncoder, Encoder, FlowDecoder, Packing
2
+
3
+ __all__ = ["AEConfig", "AutoEncoder", "Encoder", "FlowDecoder", "Packing"]
vae/ae/model.py ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image autoencoder: deterministic transformer encoder + flow-matching decoder.
2
+
3
+ * Encoder: 8x8 patches -> windowed-attention transformer blocks -> 32-channel latent
4
+ on a grid 8x smaller than the image.
5
+ * Decoder: a conditional flow-matching model. Given the latent, a noisy image ``x_t`` and
6
+ the time ``t``, it predicts the clean image ``x_0``. Images are produced by integrating
7
+ from pure noise (t=1) to the image (t=0) in a small number of steps (default 4).
8
+
9
+ Everything except the decoder's upsampling head works on token sequences, so a batch may
10
+ mix images of any sizes and aspect ratios: each image is cut into its own grid of patch
11
+ tokens, the grids are packed into one padded sequence batch (:class:`Packing`), and
12
+ attention is restricted to each image's own grid. The convolutional upsampling head runs
13
+ per group of equal-shape images.
14
+
15
+ All architecture hyper-parameters come from :class:`AEConfig`.
16
+ """
17
+ import json
18
+ import math
19
+ import os
20
+ from dataclasses import asdict, dataclass
21
+ from typing import List, Optional, Sequence, Tuple
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.nn.functional as F
26
+
27
+ from .window_attention import _tile_width, packed_window_attention
28
+
29
+
30
+ @dataclass
31
+ class AEConfig:
32
+ in_channels: int = 3
33
+ patch_size: int = 8 # spatial compression factor
34
+ latent_channels: int = 32
35
+ # encoder
36
+ enc_width: int = 1024
37
+ enc_depth: int = 8
38
+ enc_mlp_ratio: float = 2.0
39
+ # decoder
40
+ dec_width: int = 512
41
+ dec_depth: int = 6
42
+ dec_mlp_ratio: float = 4.0
43
+ dec_adaln_rank: int = 128 # rank of the per-block low-rank modulation deltas
44
+ dec_skip_blocks: int = 2 # long skip: output of block (n-1) feeds the last n blocks
45
+ dec_head_min_channels: int = 64
46
+ time_embed_dim: int = 256
47
+ # shared
48
+ head_dim: int = 64
49
+ window_radius: int = 8 # attention window: |dy| <= r and |dx| <= r tokens
50
+ rope_base: float = 100.0
51
+ # per-channel statistics of raw latents (used by encode()/decode())
52
+ latent_mean: Optional[List[float]] = None
53
+ latent_std: Optional[List[float]] = None
54
+
55
+ @classmethod
56
+ def from_json(cls, path):
57
+ with open(path) as f:
58
+ d = json.load(f)
59
+ known = {k: v for k, v in d.items() if k in cls.__dataclass_fields__}
60
+ return cls(**known)
61
+
62
+ def to_json(self, path):
63
+ with open(path, "w") as f:
64
+ json.dump(asdict(self), f, indent=2)
65
+
66
+
67
+ # --------------------------------------------------------------------------- packing
68
+
69
+ class Packing:
70
+ """Layout of a batch of token grids of sizes ``hw = [(h, w), ...]``.
71
+
72
+ Sample ``b`` is stored row-major in its own padded ``gyp x gxp`` grid at the start of a
73
+ sequence of length ``N`` (the largest padded area); its real tokens are the top-left
74
+ ``h x w`` corner. ``pos[b]`` are the sequence indices of its real tokens.
75
+ """
76
+
77
+ def __init__(self, hw: Sequence[Tuple[int, int]], radius: int, rope_base: float,
78
+ head_dim: int, device):
79
+ self.hw = [(int(h), int(w)) for h, w in hw]
80
+ WY, WX = 8, _tile_width(radius)
81
+ B = len(self.hw)
82
+ geo = [(math.ceil(h / WY) * WY, math.ceil(w / WX) * WX) for h, w in self.hw]
83
+ self.N = max(a * b for a, b in geo)
84
+ self.max_tiles = max((a // WY) * (b // WX) for a, b in geo)
85
+ self.dims = torch.tensor([[a, b, h, w] for (a, b), (h, w) in zip(geo, self.hw)],
86
+ dtype=torch.int32, device=device)
87
+ self.valid = torch.zeros(B, self.N, 1, device=device)
88
+ pos_y = torch.zeros(B, self.N, device=device)
89
+ pos_x = torch.zeros(B, self.N, device=device)
90
+ self.pos = []
91
+ for b, ((_, gxp), (h, w)) in enumerate(zip(geo, self.hw)):
92
+ yy = torch.arange(h, device=device).repeat_interleave(w)
93
+ xx = torch.arange(w, device=device).repeat(h)
94
+ p = yy * gxp + xx
95
+ self.pos.append(p)
96
+ self.valid[b, p] = 1
97
+ pos_y[b, p], pos_x[b, p] = yy.float(), xx.float()
98
+ # 2D axial RoPE tables: the head dim is split into four quarters,
99
+ # two rotate with the x coordinate, two with the y coordinate.
100
+ dq = head_dim // 4
101
+ fr = torch.exp(-math.log(rope_base) * torch.arange(dq, device=device, dtype=torch.float32) / dq)
102
+ ax, ay = pos_x[..., None] * fr, pos_y[..., None] * fr # (B, N, dq)
103
+ self._rope = [t[:, None] for t in (ax.cos(), ax.sin(), ay.cos(), ay.sin())]
104
+ self._dq = dq
105
+ self.radius = radius
106
+
107
+ def pack(self, seqs):
108
+ """list of (n_b, C) -> (B, N, C), zero padded."""
109
+ out = seqs[0].new_zeros(len(seqs), self.N, seqs[0].shape[-1])
110
+ for b, s in enumerate(seqs):
111
+ out[b, self.pos[b]] = s
112
+ return out
113
+
114
+ def unpack(self, x):
115
+ """(B, N, C) -> list of (C, h, w)."""
116
+ return [x[b, self.pos[b]].transpose(0, 1).reshape(-1, h, w) for b, (h, w) in enumerate(self.hw)]
117
+
118
+ def rope(self, q, k):
119
+ dq = self._dq
120
+ cx, sx, cy, sy = (c.to(q.dtype) for c in self._rope)
121
+
122
+ def rot(t):
123
+ a, b, c, e = t[..., :dq], t[..., dq:2 * dq], t[..., 2 * dq:3 * dq], t[..., 3 * dq:]
124
+ return torch.cat([a * cx - b * sx, a * sx + b * cx, c * cy - e * sy, c * sy + e * cy], dim=-1)
125
+
126
+ return rot(q), rot(k)
127
+
128
+ def attention(self, q, k, v):
129
+ return packed_window_attention(q, k, v, self.radius, self.dims, self.max_tiles)
130
+
131
+
132
+ def patchify(img, p):
133
+ """(C, H, W) -> (H/p * W/p, C*p*p) row-major patch tokens."""
134
+ C, H, W = img.shape
135
+ return img.reshape(C, H // p, p, W // p, p).permute(1, 3, 0, 2, 4).reshape(-1, C * p * p)
136
+
137
+
138
+ # --------------------------------------------------------------------------- encoder
139
+
140
+ class RMSNorm(nn.Module):
141
+ def __init__(self, ch, eps=1e-8):
142
+ super().__init__()
143
+ self.eps = eps
144
+ self.weight = nn.Parameter(torch.ones(ch))
145
+
146
+ def forward(self, x):
147
+ return x * x.pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt() * self.weight
148
+
149
+
150
+ class Mlp(nn.Module):
151
+ def __init__(self, i, h, o):
152
+ super().__init__()
153
+ self.m = nn.Sequential(nn.Linear(i, h), nn.GELU(), nn.Linear(h, o))
154
+
155
+ def forward(self, x):
156
+ return self.m(x)
157
+
158
+
159
+ class PlainMlp(nn.Module):
160
+ def __init__(self, d, ratio):
161
+ super().__init__()
162
+ h = int(d * ratio)
163
+ self.up = nn.Linear(d, h)
164
+ self.down = nn.Linear(h, d)
165
+ self.act = nn.GELU(approximate="tanh")
166
+
167
+ def forward(self, x):
168
+ return self.down(self.act(self.up(x)))
169
+
170
+
171
+ class EncoderBlock(nn.Module):
172
+ """Pre-norm transformer block with 2D-RoPE windowed self-attention and an MLP."""
173
+
174
+ def __init__(self, ch, head_dim, mlp_ratio):
175
+ super().__init__()
176
+ self.h = max(1, ch // head_dim)
177
+ self.norm1 = RMSNorm(ch)
178
+ self.qkv = nn.Linear(ch, ch * 3, bias=False)
179
+ self.proj = nn.Linear(ch, ch, bias=False)
180
+ self.norm2 = RMSNorm(ch)
181
+ self.mlp = PlainMlp(ch, mlp_ratio)
182
+
183
+ def forward(self, x, pk: Packing):
184
+ B, N, C = x.shape
185
+ q, k, v = self.qkv(self.norm1(x)).reshape(B, N, 3, self.h, C // self.h).permute(2, 0, 3, 1, 4)
186
+ q, k = pk.rope(q, k)
187
+ a = pk.attention(q, k, v).transpose(1, 2).reshape(B, N, C).to(x.dtype)
188
+ x = x + self.proj(a)
189
+ return x + self.mlp(self.norm2(x))
190
+
191
+
192
+ class Encoder(nn.Module):
193
+ """patch tokens (B, N, 3*p*p) -> raw latent tokens (B, N, latent_channels)."""
194
+
195
+ def __init__(self, cfg: AEConfig):
196
+ super().__init__()
197
+ w = cfg.enc_width
198
+ self.initial = nn.Linear(cfg.in_channels * cfg.patch_size ** 2, w)
199
+ self.blocks = nn.ModuleList(EncoderBlock(w, cfg.head_dim, cfg.enc_mlp_ratio)
200
+ for _ in range(cfg.enc_depth))
201
+ self.out = Mlp(w, w, cfg.latent_channels)
202
+
203
+ def forward(self, x, pk: Packing):
204
+ valid = pk.valid.to(x.dtype)
205
+ x = self.initial(x)
206
+ for b in self.blocks:
207
+ x = b(x, pk) * valid
208
+ return self.out(x)
209
+
210
+
211
+ # --------------------------------------------------------------------------- decoder
212
+
213
+ class DecoderBlock(nn.Module):
214
+ """Transformer block with windowed 2D-RoPE attention, modulated by the timestep:
215
+ the modulation vector carries (scale, gate) for the attention and the MLP sub-layers."""
216
+
217
+ def __init__(self, ch, head_dim, mlp_ratio):
218
+ super().__init__()
219
+ self.h = max(1, ch // head_dim)
220
+ self.norm1 = nn.RMSNorm(ch)
221
+ self.qkv = nn.Linear(ch, 3 * ch)
222
+ self.proj = nn.Linear(ch, ch)
223
+ self.norm2 = nn.RMSNorm(ch)
224
+ hid = int(ch * mlp_ratio)
225
+ self.fc1 = nn.Linear(ch, hid)
226
+ self.fc2 = nn.Linear(hid, ch)
227
+
228
+ def forward(self, x, mod, pk: Packing):
229
+ B, N, C = x.shape
230
+ s1, g1, s2, g2 = mod[:, None, :].chunk(4, dim=-1)
231
+ h = self.norm1(x) * (1 + s1)
232
+ q, k, v = self.qkv(h).reshape(B, N, 3, self.h, C // self.h).permute(2, 0, 3, 1, 4)
233
+ q, k = pk.rope(q, k)
234
+ a = pk.attention(q, k, v).transpose(1, 2).reshape(B, N, C).to(h.dtype)
235
+ x = x + g1 * self.proj(a)
236
+ h = self.norm2(x) * (1 + s2)
237
+ return x + g2 * self.fc2(F.gelu(self.fc1(h)))
238
+
239
+
240
+ class FlowDecoder(nn.Module):
241
+ """x0-prediction network: (x_t, t, latent) -> predicted clean image.
242
+
243
+ The transformer works on packed tokens; the upsampling head (PixelShuffle + 3x3 convs)
244
+ runs per group of equal-shape samples.
245
+ """
246
+
247
+ def __init__(self, cfg: AEConfig):
248
+ super().__init__()
249
+ D, p = cfg.dec_width, cfg.patch_size
250
+ self.depth = cfg.dec_depth
251
+ self.n_skip = cfg.dec_skip_blocks
252
+ self.time_embed_dim = cfg.time_embed_dim
253
+ self.patchify = nn.Linear(cfg.in_channels * p * p, D)
254
+ self.latent_up = nn.Linear(cfg.latent_channels, D)
255
+ self.fuse_in = nn.Linear(2 * D, D)
256
+ self.tmlp = nn.Sequential(nn.Linear(cfg.time_embed_dim, D), nn.SiLU(), nn.Linear(D, D))
257
+ self.adaln_act = nn.SiLU()
258
+ mw = 4 * D
259
+ self.adaln_base = nn.Linear(D, mw)
260
+ self.d_down = nn.ModuleList(nn.Linear(D, cfg.dec_adaln_rank, bias=False) for _ in range(self.depth))
261
+ self.d_up = nn.ModuleList(nn.Linear(cfg.dec_adaln_rank, mw, bias=False) for _ in range(self.depth))
262
+ self.blocks = nn.ModuleList(DecoderBlock(D, cfg.head_dim, cfg.dec_mlp_ratio)
263
+ for _ in range(self.depth))
264
+ self.fuse_skip = nn.Linear(2 * D, D)
265
+ # progressive upsampling head: [PixelShuffle(2) -> conv3x3 -> GELU] x log2(p)
266
+ n = int(round(math.log2(p)))
267
+ ch, stages = D, []
268
+ for _ in range(n):
269
+ nxt = max(ch // 2, cfg.dec_head_min_channels)
270
+ stages.append(nn.Sequential(nn.PixelShuffle(2), nn.Conv2d(ch // 4, nxt, 3, padding=1), nn.GELU()))
271
+ ch = nxt
272
+ self.up_stages = nn.ModuleList(stages)
273
+ self.up_final = nn.Conv2d(ch, cfg.in_channels, 3, padding=1)
274
+
275
+ def time_embed(self, t):
276
+ half = self.time_embed_dim // 2
277
+ freqs = torch.exp(-math.log(10000.0) * torch.arange(half, device=t.device, dtype=torch.float32) / half)
278
+ a = t.float()[:, None] * 1000.0 * freqs[None]
279
+ return self.tmlp(torch.cat([a.sin(), a.cos()], dim=-1).to(self.tmlp[0].weight.dtype))
280
+
281
+ def head(self, feats):
282
+ """feats: (B, D, h, w) -> (B, 3, h*p, w*p)."""
283
+ for st in self.up_stages:
284
+ feats = st(feats)
285
+ return self.up_final(feats)
286
+
287
+ def forward(self, xt, t, z, pk: Packing):
288
+ """xt: packed noisy patch tokens (B, N, 3*p*p); t: (B,); z: packed latents (B, N, C).
289
+ Returns a list of per-sample x0 predictions (3, H_b, W_b)."""
290
+ valid = pk.valid
291
+ cond = self.adaln_act(self.time_embed(t))
292
+ base = self.adaln_base(cond)
293
+ x = self.fuse_in(torch.cat([self.patchify(xt), self.latent_up(z)], dim=-1))
294
+ valid = valid.to(x.dtype)
295
+ x = x * valid
296
+ outs = []
297
+ for i, b in enumerate(self.blocks):
298
+ if i == self.depth - self.n_skip:
299
+ x = self.fuse_skip(torch.cat([outs[self.n_skip - 1], x], dim=-1))
300
+ x = b(x, base + self.d_up[i](self.d_down[i](cond)), pk) * valid
301
+ outs.append(x)
302
+ feats = pk.unpack(x)
303
+ preds = [None] * len(feats)
304
+ groups = {}
305
+ for i, f in enumerate(feats):
306
+ groups.setdefault(tuple(f.shape), []).append(i)
307
+ for idx in groups.values():
308
+ y = self.head(torch.stack([feats[i] for i in idx]))
309
+ for j, i in enumerate(idx):
310
+ preds[i] = y[j]
311
+ return preds
312
+
313
+
314
+ # --------------------------------------------------------------------------- wrapper
315
+
316
+ def _as_list(x):
317
+ """Tensor (B, C, H, W) or list of (C, H, W) -> (list, was_tensor)."""
318
+ if torch.is_tensor(x):
319
+ assert x.dim() == 4, f"expected (B, C, H, W), got {tuple(x.shape)}"
320
+ return list(x.unbind(0)), True
321
+ return list(x), False
322
+
323
+
324
+ class AutoEncoder(nn.Module):
325
+ """Encoder + flow-matching decoder with built-in latent normalization.
326
+
327
+ ``encode`` returns latents normalized per channel ((z - mean) / std), ready to be used
328
+ as a diffusion target; ``decode`` expects such latents. ``encode_raw`` / ``decode_raw``
329
+ skip the normalization.
330
+
331
+ All four methods take either a batch tensor ``(B, C, H, W)`` (returns a tensor) or a
332
+ list of ``(C, H_i, W_i)`` tensors of different sizes (returns a list), processed in one
333
+ forward pass.
334
+ """
335
+
336
+ def __init__(self, cfg: AEConfig):
337
+ super().__init__()
338
+ self.config = cfg
339
+ self.encoder = Encoder(cfg)
340
+ self.decoder = FlowDecoder(cfg)
341
+ C = cfg.latent_channels
342
+ mean = torch.tensor(cfg.latent_mean, dtype=torch.float32) if cfg.latent_mean else torch.zeros(C)
343
+ std = torch.tensor(cfg.latent_std, dtype=torch.float32) if cfg.latent_std else torch.ones(C)
344
+ self.register_buffer("latent_mean", mean.view(C, 1, 1), persistent=False)
345
+ self.register_buffer("latent_std", std.view(C, 1, 1), persistent=False)
346
+ # compute dtype: encoder weights are stored in it; the decoder keeps fp32 weights and
347
+ # runs under autocast with this dtype (when it is not float32).
348
+ self.compute_dtype = torch.float32
349
+
350
+ # ---------------------------------------------------------------- loading
351
+ @classmethod
352
+ def from_pretrained(cls, path_or_repo, device=None, dtype=None, **hub_kwargs):
353
+ """Load from a local directory or a Hugging Face repo id containing
354
+ ``config.json`` and ``model.safetensors``.
355
+
356
+ dtype: compute dtype (default: bfloat16 on CUDA, float32 on CPU).
357
+ """
358
+ from safetensors.torch import load_file
359
+
360
+ if os.path.isdir(path_or_repo):
361
+ cfg_path = os.path.join(path_or_repo, "config.json")
362
+ w_path = os.path.join(path_or_repo, "model.safetensors")
363
+ else:
364
+ from huggingface_hub import hf_hub_download
365
+ cfg_path = hf_hub_download(path_or_repo, "config.json", **hub_kwargs)
366
+ w_path = hf_hub_download(path_or_repo, "model.safetensors", **hub_kwargs)
367
+ device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
368
+ if dtype is None:
369
+ dtype = torch.bfloat16 if device.type == "cuda" else torch.float32
370
+ model = cls(AEConfig.from_json(cfg_path))
371
+ model.load_state_dict(load_file(w_path), strict=True)
372
+ model.to(device).eval().requires_grad_(False)
373
+ model.set_dtype(dtype)
374
+ return model
375
+
376
+ def set_dtype(self, dtype):
377
+ self.compute_dtype = dtype
378
+ self.encoder.to(dtype)
379
+ self.decoder.float()
380
+ return self
381
+
382
+ @property
383
+ def device(self):
384
+ return self.latent_mean.device
385
+
386
+ def _autocast(self):
387
+ return torch.autocast(self.device.type, dtype=self.compute_dtype,
388
+ enabled=self.compute_dtype != torch.float32)
389
+
390
+ def _packing(self, hw):
391
+ c = self.config
392
+ return Packing(hw, c.window_radius, c.rope_base, c.head_dim, self.device)
393
+
394
+ # ---------------------------------------------------------------- encoder side
395
+ @torch.no_grad()
396
+ def encode_raw(self, images):
397
+ """images: (B, 3, H, W) or a list of (3, H_i, W_i); float in [-1, 1] or uint8 in
398
+ [0, 255]; sides multiples of the patch size.
399
+ Returns raw (unnormalized) float32 latents: (B, C, H/p, W/p) or a list of (C, H_i/p, W_i/p)."""
400
+ p = self.config.patch_size
401
+ imgs, was_tensor = _as_list(images)
402
+ for im in imgs:
403
+ assert im.dim() == 3 and im.shape[-2] % p == 0 and im.shape[-1] % p == 0, \
404
+ f"each image must be (3, H, W) with H, W multiples of {p}, got {tuple(im.shape)}"
405
+ pk = self._packing([(im.shape[-2] // p, im.shape[-1] // p) for im in imgs])
406
+ toks = []
407
+ for im in imgs:
408
+ im = im.to(self.device)
409
+ im = im.float() / 127.5 - 1.0 if im.dtype == torch.uint8 else im
410
+ toks.append(patchify(im.to(self.compute_dtype), p))
411
+ z = [t.float() for t in pk.unpack(self.encoder(pk.pack(toks), pk))]
412
+ return torch.stack(z) if was_tensor else z
413
+
414
+ @torch.no_grad()
415
+ def encode(self, images):
416
+ """Images -> normalized latents (zero mean / unit std per channel)."""
417
+ z = self.encode_raw(images)
418
+ if torch.is_tensor(z):
419
+ return (z - self.latent_mean) / self.latent_std
420
+ return [(t - self.latent_mean) / self.latent_std for t in z]
421
+
422
+ # ---------------------------------------------------------------- decoder side
423
+ @torch.no_grad()
424
+ def decode_raw(self, latents, steps=4, generator=None, noise=None):
425
+ """Raw latents -> images (approximately in [-1, 1], not clamped).
426
+
427
+ latents: (B, C, h, w) or a list of (C, h_i, w_i).
428
+ steps: number of flow-matching integration steps (default 4; 1 gives a fast,
429
+ softer result, more steps add a bit of detail).
430
+ generator: optional torch.Generator for the initial noise (on the model device).
431
+ noise: optional explicit initial noise, (B, 3, H, W) or a list of (3, H_i, W_i).
432
+ """
433
+ p, C = self.config.patch_size, self.config.in_channels
434
+ zs, was_tensor = _as_list(latents)
435
+ zs = [z.to(self.device, torch.float32) for z in zs]
436
+ pk = self._packing([tuple(z.shape[-2:]) for z in zs])
437
+ zp = pk.pack([z.flatten(1).transpose(0, 1) for z in zs])
438
+ if noise is not None:
439
+ xs = [n.to(self.device, torch.float32) for n in _as_list(noise)[0]]
440
+ elif was_tensor:
441
+ B, _, h, w = latents.shape
442
+ xs = list(torch.randn((B, C, h * p, w * p), generator=generator, device=self.device).unbind(0))
443
+ else:
444
+ xs = [torch.randn((C, z.shape[1] * p, z.shape[2] * p), generator=generator, device=self.device)
445
+ for z in zs]
446
+ ts = torch.linspace(1.0, 0.0, steps + 1, device=self.device)
447
+ B = len(zs)
448
+ for i in range(steps):
449
+ with self._autocast():
450
+ xt = pk.pack([patchify(x.to(self.compute_dtype), p) for x in xs])
451
+ preds = self.decoder(xt, ts[i].expand(B), zp, pk)
452
+ # x0 prediction -> implied noise -> move along the straight path to ts[i+1]
453
+ xs = [ts[i + 1] * (x - (1 - ts[i]) * pr.float()) / ts[i] + (1 - ts[i + 1]) * pr.float()
454
+ for x, pr in zip(xs, preds)]
455
+ return torch.stack(xs) if was_tensor else xs
456
+
457
+ @torch.no_grad()
458
+ def decode(self, latents, steps=4, generator=None, noise=None):
459
+ """Normalized latents (as returned by ``encode``) -> images in about [-1, 1]."""
460
+ if torch.is_tensor(latents):
461
+ z = latents.to(self.device, torch.float32) * self.latent_std + self.latent_mean
462
+ else:
463
+ z = [t.to(self.device, torch.float32) * self.latent_std + self.latent_mean for t in latents]
464
+ return self.decode_raw(z, steps=steps, generator=generator, noise=noise)
465
+
466
+ @torch.no_grad()
467
+ def forward(self, images, steps=4, generator=None):
468
+ """Round trip: images -> latents -> images."""
469
+ return self.decode(self.encode(images), steps=steps, generator=generator)
vae/ae/window_attention.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local 2D window attention.
2
+
3
+ Every token attends to all tokens within a square window of Chebyshev radius ``r``
4
+ (i.e. ``|dy| <= r and |dx| <= r``) on its 2D grid.
5
+
6
+ Two numerically equivalent implementations are provided:
7
+
8
+ * a fused Triton kernel (CUDA only, forward pass only);
9
+ * PyTorch ``scaled_dot_product_attention`` with a boolean window mask (CPU, or when
10
+ Triton is unavailable).
11
+
12
+ Both work on a packed batch: every sample is its own token grid of any size.
13
+ """
14
+ import torch
15
+ import torch.nn.functional as F
16
+
17
+ try: # Triton is optional; without it the SDPA path is used.
18
+ import triton
19
+ import triton.language as tl
20
+
21
+ _HAS_TRITON = True
22
+ except Exception: # pragma: no cover
23
+ _HAS_TRITON = False
24
+
25
+ _LOG2E = 1.4426950408889634
26
+
27
+ # Global switch: "auto" (kernel when possible), "sdpa" (always masked SDPA).
28
+ BACKEND = "auto"
29
+
30
+
31
+ if _HAS_TRITON:
32
+
33
+ @triton.jit
34
+ def _window_attn_fwd(Q, K, V, O, DIMS, NH: tl.constexpr, NMAX, WY: tl.constexpr,
35
+ WX: tl.constexpr, R: tl.constexpr, D: tl.constexpr,
36
+ BR: tl.constexpr, qk_scale):
37
+ # One program = one WY x WX tile of queries for one (batch, head).
38
+ # Keys are streamed from the tile extended by R on every side ("halo").
39
+ t, bh = tl.program_id(0), tl.program_id(1)
40
+ b = bh // NH
41
+ gyp = tl.load(DIMS + b * 4 + 0)
42
+ gxp = tl.load(DIMS + b * 4 + 1)
43
+ gyr = tl.load(DIMS + b * 4 + 2)
44
+ gxr = tl.load(DIMS + b * 4 + 3)
45
+ ntx = gxp // WX
46
+ if t >= (gyp // WY) * ntx:
47
+ return
48
+ ty, tx = t // ntx, t % ntx
49
+ base = bh * NMAX * D
50
+ SX: tl.constexpr = WX + 2 * R
51
+ SY: tl.constexpr = WY + 2 * R
52
+
53
+ qi = tl.arange(0, WY * WX)
54
+ qyr, qxr = qi // WX, qi % WX
55
+ qy, qx = ty * WY + qyr, tx * WX + qxr
56
+ d = tl.arange(0, D)
57
+ qo = base + (qy * gxp + qx)[:, None] * D + d[None, :]
58
+ q = tl.load(Q + qo)
59
+ okq = (qy < gyr) & (qx < gxr)
60
+
61
+ kxr = tl.arange(0, BR * SX) % SX
62
+ okx = okq[:, None] & (tl.abs(qxr[:, None] - (kxr[None, :] - R)) <= R)
63
+ kxg = tx * WX - R + kxr
64
+ okg = (kxg >= 0) & (kxg < gxr)
65
+
66
+ m = tl.full((WY * WX,), -1e30, tl.float32)
67
+ l = tl.zeros((WY * WX,), tl.float32)
68
+ acc = tl.zeros((WY * WX, D), tl.float32)
69
+
70
+ for r0 in range(0, SY, BR):
71
+ kyr = r0 + tl.arange(0, BR * SX) // SX
72
+ kyg = ty * WY - R + kyr
73
+ ok = okg & (kyg >= 0) & (kyg < gyr) & (kyr < SY)
74
+ off = base + (kyg * gxp + kxg) * D
75
+ k = tl.load(K + off[:, None] + d[None, :], mask=ok[:, None], other=0.0)
76
+ v = tl.load(V + off[:, None] + d[None, :], mask=ok[:, None], other=0.0)
77
+
78
+ s = tl.dot(q, tl.trans(k)) * qk_scale
79
+ good = okx & (tl.abs(qyr[:, None] - (kyr[None, :] - R)) <= R) & ok[None, :]
80
+ s = tl.where(good, s, -1e30)
81
+
82
+ mn = tl.maximum(m, tl.max(s, 1))
83
+ p = tl.where(good, tl.math.exp2(s - mn[:, None]), 0.0)
84
+ a = tl.math.exp2(m - mn)
85
+ l = l * a + tl.sum(p, 1)
86
+ acc = acc * a[:, None] + tl.dot(p.to(v.dtype), v).to(tl.float32)
87
+ m = mn
88
+
89
+ lc = tl.maximum(l, 1e-30)
90
+ tl.store(O + qo, (acc / lc[:, None]).to(O.dtype.element_ty))
91
+
92
+
93
+ def _tile_width(r):
94
+ # The kernel needs a power-of-two key strip width (WX + 2R).
95
+ for wx in (16, 8):
96
+ sx = wx + 2 * r
97
+ if sx & (sx - 1) == 0:
98
+ return wx
99
+ raise ValueError(f"window radius {r}: no supported tile width")
100
+
101
+
102
+ @torch._dynamo.disable
103
+ def packed_window_attention(q, k, v, r, dims, max_tiles):
104
+ """Windowed attention for a packed batch of differently shaped token grids.
105
+
106
+ Sample ``b`` is stored row-major in a padded ``gyp x gxp`` grid at the start of the
107
+ sequence, with its real ``gyr x gxr`` tokens in the top-left corner; the rest is padding.
108
+ dims: (B, 4) int32 ``[gyp, gxp, gyr, gxr]`` (gyp a multiple of 8, gxp of the tile width).
109
+ q, k, v: (B, heads, Nmax, head_dim). Padding outputs are zero (kernel) or unused (SDPA).
110
+ """
111
+ B, nh, N, D = q.shape
112
+ if BACKEND == "auto" and _HAS_TRITON and q.is_cuda and D in (16, 32, 64, 128):
113
+ q, k, v = (t.to(torch.bfloat16).contiguous() for t in (q, k, v))
114
+ o = torch.zeros_like(q)
115
+ _window_attn_fwd[(max_tiles, B * nh)](
116
+ q, k, v, o, dims.to(q.device, torch.int32).contiguous(), NH=nh, NMAX=N, WY=8,
117
+ WX=_tile_width(r), R=r, D=D, BR=4, qk_scale=D ** -0.5 * _LOG2E, num_warps=8, num_stages=3)
118
+ return o
119
+ # reference path: per-sample block mask over the packed sequence
120
+ n = torch.arange(N, device=q.device)
121
+ d = dims.to(q.device).long()
122
+ yy, xx = n[None] // d[:, 1:2], n[None] % d[:, 1:2] # (B, N)
123
+ valid = (yy < d[:, 2:3]) & (xx < d[:, 3:4])
124
+ win = ((yy[:, :, None] - yy[:, None]).abs() <= r) & ((xx[:, :, None] - xx[:, None]).abs() <= r)
125
+ m = win & valid[:, None, :]
126
+ m = m | (~valid[:, :, None] & torch.eye(N, dtype=torch.bool, device=q.device))
127
+ return F.scaled_dot_product_attention(q.contiguous(), k.contiguous(), v.contiguous(),
128
+ attn_mask=m[:, None])
129
+
vae/config.json ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "in_channels": 3,
3
+ "patch_size": 8,
4
+ "latent_channels": 32,
5
+ "enc_width": 1024,
6
+ "enc_depth": 8,
7
+ "enc_mlp_ratio": 2.0,
8
+ "dec_width": 512,
9
+ "dec_depth": 6,
10
+ "dec_mlp_ratio": 4.0,
11
+ "dec_adaln_rank": 128,
12
+ "dec_skip_blocks": 2,
13
+ "dec_head_min_channels": 64,
14
+ "time_embed_dim": 256,
15
+ "head_dim": 64,
16
+ "window_radius": 8,
17
+ "rope_base": 100.0,
18
+ "latent_mean": [
19
+ -4.137261,
20
+ 2.725146,
21
+ -1.435803,
22
+ -2.187023,
23
+ -4.161275,
24
+ -1.58918,
25
+ -2.527523,
26
+ 3.002302,
27
+ 2.793705,
28
+ -2.091501,
29
+ -2.632896,
30
+ -3.59589,
31
+ -3.069669,
32
+ -4.568713,
33
+ 3.26668,
34
+ -0.897648,
35
+ 2.072541,
36
+ 5.451158,
37
+ 1.747871,
38
+ -2.113284,
39
+ 2.03444,
40
+ -1.379274,
41
+ 0.66369,
42
+ 2.712183,
43
+ -0.111181,
44
+ 6.054838,
45
+ 1.235881,
46
+ -2.02777,
47
+ 3.663243,
48
+ -2.454632,
49
+ 1.141335,
50
+ 1.786136
51
+ ],
52
+ "latent_std": [
53
+ 1.030738,
54
+ 0.951682,
55
+ 1.079687,
56
+ 0.957871,
57
+ 1.198266,
58
+ 1.102266,
59
+ 1.213605,
60
+ 0.947544,
61
+ 1.085991,
62
+ 1.093223,
63
+ 0.9027,
64
+ 1.010205,
65
+ 0.920466,
66
+ 1.044107,
67
+ 1.023179,
68
+ 0.992978,
69
+ 0.991627,
70
+ 1.073585,
71
+ 1.114629,
72
+ 1.056403,
73
+ 1.093012,
74
+ 1.015109,
75
+ 1.007069,
76
+ 0.976026,
77
+ 0.982324,
78
+ 1.093805,
79
+ 1.036086,
80
+ 1.005727,
81
+ 1.104078,
82
+ 1.042298,
83
+ 1.095522,
84
+ 0.960255
85
+ ]
86
+ }
vae/example.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal round trip: image -> latents -> image."""
2
+ import sys
3
+
4
+ import numpy as np
5
+ import torch
6
+ from PIL import Image
7
+
8
+ from ae import AutoEncoder
9
+
10
+ src = sys.argv[1] if len(sys.argv) > 1 else "input.png"
11
+ dst = sys.argv[2] if len(sys.argv) > 2 else "recon.png"
12
+
13
+ model = AutoEncoder.from_pretrained("Muinez/f8c32-ae") # HF repo id or local dir
14
+
15
+ img = Image.open(src).convert("RGB")
16
+ w, h = (img.width // 8) * 8, (img.height // 8) * 8 # sides must be multiples of 8
17
+ img = img.crop((0, 0, w, h))
18
+ x = torch.from_numpy(np.asarray(img, dtype=np.float32)).permute(2, 0, 1)[None] / 127.5 - 1
19
+
20
+ z = model.encode(x) # (1, 32, h/8, w/8), normalized
21
+ print("latent", tuple(z.shape), f"mean {z.mean():.3f} std {z.std():.3f}")
22
+
23
+ gen = torch.Generator(model.device).manual_seed(0)
24
+ y = model.decode(z, steps=4, generator=gen).clamp(-1, 1)
25
+
26
+ mse = (y.cpu() - x).pow(2).mean()
27
+ print(f"PSNR {10 * torch.log10(4 / mse):.2f} dB")
28
+ Image.fromarray(((y[0] + 1) * 127.5).round().byte().permute(1, 2, 0).cpu().numpy()).save(dst)
29
+ print("saved", dst)
vae/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a484ad128d0837630dd16248af9b2265d2721d7434dc696bf7451fcbf1809665
3
+ size 184623110