recoilme commited on
Commit
e995b65
·
verified ·
1 Parent(s): 76690b4

Ra sdxs-VAE (step 61000)

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
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ pipeline_tag: text-to-image
4
+ tags: [text-to-image, flow-matching, pyramidal-unet, latent-diffusion, asym-vae, from-scratch, ra, udit]
5
+ ---
6
+
7
+ # Ra2
8
+
9
+ **Ra2** — second-generation layout of Ra: the transformer no longer carries a pixel-space refiner, so the
10
+ model is a plain latent diffusion model and the image is decoded by a **separate asymmetric VAE**
11
+ (f8 encoder / f16 decoder → 512×512 from a 32×32 latent).
12
+
13
+ It is a 256→512 text-to-image model trained from scratch: a pyramidal U-transformer (240M, 40 blocks)
14
+ flow-matching in an 8×/32-channel latent, a frozen Ettin-150M text encoder with a learnable layer-slice
15
+ mixer (10.0M), and a **separate asymmetric VAE** that decodes the latent into pixels.
16
+
17
+ | | |
18
+ |---|---|
19
+ | output | 512×512 from a 32×32 latent (encoder f8, decoder f16) |
20
+ | sampler | 16 Euler steps, cfg 2.5 |
21
+ | latent | 32 channels from the bundled `vae/` (AsymmetricAutoencoderKL) |
22
+ | text encoder | [`jhu-clsp/ettin-encoder-150m`](https://huggingface.co/jhu-clsp/ettin-encoder-150m), frozen, bundled in `text_encoder/` |
23
+ | body | 40 blocks — L0: 6 conv + 8 attention, L1: 9, L2: 9, L3: 4 (widths 192→512→1024→1536) |
24
+ | training | latent flow matching only (MSE in latent space); the VAE decodes, no pixel-space refiner |
25
+ | licence | MIT |
26
+
27
+ ## Design
28
+
29
+ The body is a U-stack: width grows as tokens shrink, so expensive attention runs on 16–64 tokens while
30
+ the 1024-token grid keeps 3×3 convolutions, and skip connections add every level back in the decoder.
31
+ One L0-attention block costs 0.86 GMAC (3.9% of the body) at 0.37M parameters, which is why extra blocks
32
+ were added at full resolution rather than in depth.
33
+
34
+ Positions: 2D RoPE inside attention plus an absolute 2D sin/cos embedding projected into the level width
35
+ by a zero-initialised linear layer, so new blocks can be grafted onto a trained model without changing
36
+ its output (verified bit-identical: `Body.graft`).
37
+
38
+ Decoding is plain: the latent goes through the VAE decoder once. That keeps the objective a clean latent
39
+ MSE — the price is that fine detail must live in the 32×32 latent.
40
+
41
+ ## Training
42
+
43
+ | stage | what | steps |
44
+ |---|---|---|
45
+ | 1 | FLUX-Reason-6M, 256², old f8c32 AE + pixel-space refiner (23.9M) | 560 142 |
46
+ | 2 | same data, capacity graft 26 → 36 blocks, absolute-PE branch | 404 000 |
47
+ | 3 | switch to the asymmetric VAE (32×32 latent → 512²), latent-only loss, MJ shard | 5 000 |
48
+ | 4 | butterfly fine-tune, 966 images, zero-init graft 36 → 40 blocks (3+3 L0 attention) + EMA | 25 000 |
49
+
50
+ The bundled `transformer/` is the stage-4 checkpoint (step 61 000 of the current counter). It is a
51
+ **research stage**, not a general-purpose model: it was trained to fit one narrow dataset.
52
+
53
+ ## Examples
54
+
55
+ The current checkpoint on its training domain (butterfly specimens, 512² downscaled to 256²), and the
56
+ same species in the source data:
57
+
58
+ <img src="docs/img/samples.png" width="100%"/>
59
+
60
+ ## Usage
61
+
62
+ ```bash
63
+ python3 src/sample.py --prompt "butterfly, Troides hypolitus" --out out.png
64
+ ```
65
+
66
+ ## Training / fine-tuning
67
+
68
+ ```bash
69
+ bash run.sh /path/to/tfrecord_shards 10 # 10 эпох: TFRecord-шарды i1 (PNG 256² + подписи)
70
+ python3 src/train.py # дефолты: --init transformer, --out tmp/run, --target 708000
71
+ ```
72
+
73
+ ## Limitations
74
+
75
+ - Butterflies only, right now: 966 training images, 3-word captions.
76
+ - Colour drifts towards saturated species stereotypes; vein pattern is approximate, not faithful.
77
+ - No text rendering, no faces, no general scenes. Research artifact — no safety tuning.
78
+
79
+ MIT for our code and weights (`src/`, `transformer/`). The bundled VAE is `AsymmetricAutoencoderKL`
80
+ (32 latent channels, encoder f8 / decoder f16); the text encoder is MIT.
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
+ Размеры: тело **240.0M** (40 блоков), миксер 10.0M, VAE отдельно (асимметричный: энкодер f8 / декодер f16). Шаг обучения 97 мс при bs8 на 256² (8.1 ГБ). Для сравнения, прежний
29
+ плоский стек ширины 1024 на всех уровнях: 196 мс и hf 0.18 против 0.54 у пирамиды (код и чекпойнт
30
+ той архитектуры удалены как промежуточный артефакт).
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
+ | тело | 240.0M (40 блоков) |
67
+ | выбор слоёв текста | 10.04M |
68
+ | VAE | отдельно, `vae/` (AsymmetricAutoencoderKL) |
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/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: 8597cff8cfadc6a9203b73ec2f041bcc51ca500ba77f74cbb8e670043aea8cb6
  • Pointer size: 131 Bytes
  • Size of remote file: 914 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>
requirements.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ra — что нужно, чтобы обучать и сэмплить.
2
+ # Версии не прибиты: нижние границы — то, с чем реально работает код.
3
+ # (проверено на torch 2.6 / transformers 5.5 / diffusers 0.41 / hf_hub 1.33)
4
+
5
+ # ядро: модель, обучение, сэмплинг
6
+ torch>=2.4
7
+ numpy>=1.24
8
+ Pillow>=10.0
9
+ safetensors>=0.4
10
+ huggingface_hub>=0.27 # hf download / HfFileSystem (поток шардов и датасетов)
11
+ transformers>=4.48 # ModernBertModel (текстовый энкодер Ettin)
12
+
13
+ # поток данных FLUX-Reason-6M (parquet)
14
+ datasets>=2.19
15
+ pyarrow>=14.0
16
+
17
+ # 8-битный AdamW; без него код автоматически перейдёт на обычный AdamW
18
+ bitsandbytes>=0.43
19
+
20
+ # необязательно: triton ускоряет оконное внимание внутри нашего VAE (vae/ae/window_attention.py).
21
+ # Без него работает путь на SDPA — тише, но немного медленнее. Обычно приходит вместе с torch.
22
+ # triton>=2.1
23
+
24
+ # ВАЖНО: сторонний VAE sdxs (vae_sdxs/, AsymmetricAutoencoderKL) требует diffusers >= 0.37.
25
+ # Наш собственный VAE (vae/, f8c32) идёт своим кодом в vae/ae/ и diffusers не требует.
26
+ # Если ставишь diffusers отдельно и не нужен sdxs — эту строку можно убрать.
27
+ diffusers>=0.37
src/export.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Экспорт обученного UDM в `transformer/`: safetensors + config.json.
2
+
3
+ Три части в одном файле, по префиксам:
4
+ body.* — пирамидальное тело, поток в латентном пространстве VAE
5
+ text_mixer.* — обучаемый выбор слоёв Ettin (SliceMixer)
6
+
7
+ python3 src/export.py --ckpt tmp/run256/last.pt --out transformer
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ from pathlib import Path
14
+
15
+ import torch
16
+ from safetensors.torch import save_file
17
+
18
+ ROOT = Path(__file__).resolve().parent.parent
19
+
20
+
21
+ def main():
22
+ ap = argparse.ArgumentParser()
23
+ ap.add_argument("--ckpt", default=str(ROOT / "tmp" / "run256" / "last.pt"))
24
+ ap.add_argument("--out", default=str(ROOT / "transformer"))
25
+ ap.add_argument("--metrics", default="", help="дописать метрики в config.json (строка)")
26
+ ap.add_argument("--half", action="store_true", help="хранить веса в fp16 (вдвое меньше, для публикации)")
27
+ args = ap.parse_args()
28
+
29
+ ck = torch.load(args.ckpt, map_location="cpu")
30
+ tensors = {}
31
+ body_sd = ck.get("body_ema") or ck["body"] # для публикации берём EMA, если обучен
32
+ mixer_sd = ck.get("mixer_ema") or ck["mixer"]
33
+ for name, sd in (("body", body_sd), ("text_mixer", mixer_sd)):
34
+ for k, v in sd.items():
35
+ tensors[f"{name}.{k}"] = (v.half() if args.half else v).contiguous()
36
+ out = Path(args.out)
37
+ out.mkdir(parents=True, exist_ok=True)
38
+ save_file(tensors, str(out / "diffusion_pytorch_model.safetensors"))
39
+
40
+ ae_cfg = json.loads((ROOT / "vae" / "config.json").read_text()) # AsymmetricAutoencoderKL
41
+ ta = ck.get("args") or {}
42
+ counts = {} # сколько блоков в каждом списке — по ключам тензоров
43
+ for k in ck["body"]:
44
+ g = k.split(".")[0]
45
+ if g[0] in "ed" and "." in k:
46
+ idx = k.split(".")[1]
47
+ if idx.isdigit():
48
+ counts.setdefault(g, set()).add(idx)
49
+ nblk = {g: len(v) for g, v in counts.items()}
50
+ # конфиг описывает ТОЛЬКО тело; статистики латента — в vae/config.json, шаг — параметр запуска
51
+ cfg = {
52
+ "_class_name": "UDiT",
53
+ "format": "udm/v1",
54
+ "storage_dtype": "float16" if args.half else "float32",
55
+ "latent_channels": ae_cfg["latent_channels"],
56
+ "text_dim": ta.get("text_dim", 768),
57
+ "body": {
58
+ "kind": "пирамидальный U: ширина растёт к низу",
59
+ "widths": [192, 512, 1024, 1536],
60
+ "heads": [6, 8, 8, 8],
61
+ "blocks": nblk,
62
+ "levels": [
63
+ {"level": 0, "tokens": "÷1", "width": 192,
64
+ "blocks": "conv 6 enc / 4 dec + 2 self-attn на сторону"},
65
+ {"level": 1, "tokens": "÷2", "width": 512, "blocks": "attn 5 enc / 4 dec"},
66
+ {"level": 2, "tokens": "÷4", "width": 1024, "blocks": "attn 5 enc / 4 dec"},
67
+ {"level": 3, "tokens": "÷8", "width": 1536, "blocks": "attn 4 (боттлнек)"},
68
+ ],
69
+ "skips": "аддитивные (SD-стиль): e0/e1/e2 прибавляются в декодере",
70
+ "down": "mean-pool 2x + Linear",
71
+ "up": "Linear + nearest 2x",
72
+ "positions": {
73
+ "rope": {"kind": "2d", "base": 100, "phase": "в латентных пикселях на каждом уровне"},
74
+ "absolute": {"kind": "2d sincos", "dim": 64,
75
+ "into": "zero-init Linear(64 -> width) к выходу stem, в паре с RoPE"},
76
+ },
77
+ "text": "кросс-внимание на всех уровнях",
78
+ "parametrization": "x0 латента",
79
+ "params": round(sum(v.numel() for k, v in tensors.items() if k.startswith("body.")) / 1e6, 2),
80
+ },
81
+ }
82
+ (out / "config.json").write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
83
+
84
+ print(f"сохранено: {out}/diffusion_pytorch_model.safetensors "
85
+ f"({sum(v.numel() for v in tensors.values())/1e6:.1f}M параметров, "
86
+ f"{(out / 'diffusion_pytorch_model.safetensors').stat().st_size/1e6:.0f} МБ)")
87
+ print(f"тело {cfg['body']['params']}M, блоки {nblk}")
88
+
89
+
90
+ def load_exported(model, enc, ae, path, device=None):
91
+ """Загрузить из `transformer/` (config.json + safetensors с префиксами body./text_mixer.)."""
92
+ import json
93
+ from safetensors.torch import load_file
94
+ path = Path(path)
95
+ sd = load_file(str(path / "diffusion_pytorch_model.safetensors"), device=str(device) if device else None)
96
+ def sub(prefix):
97
+ return {k[len(prefix) + 1:]: v for k, v in sd.items() if k.startswith(prefix + ".")}
98
+ model.body.load_state_dict(sub("body"), strict=False)
99
+ enc.mixer.load_state_dict(sub("text_mixer"))
100
+ cfg_path = path / "config.json"
101
+ return json.loads(cfg_path.read_text()) if cfg_path.exists() else {}
102
+
103
+
104
+ if __name__ == "__main__":
105
+ 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,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
32
+ DEV = "cuda" if torch.cuda.is_available() else "cpu"
33
+ _ROPE_CACHE: dict = {}
34
+
35
+
36
+ def t_embed(t: torch.Tensor, dim: int = 256) -> torch.Tensor:
37
+ """Синусоидальный эмбеддинг времени (t в [0,1] умножается на 1000)."""
38
+ half = dim // 2
39
+ f = torch.exp(-math.log(10000.0) * torch.arange(half, device=t.device, dtype=torch.float32) / half)
40
+ a = (t.float()[:, None] * 1000.0) * f[None]
41
+ return torch.cat([a.sin(), a.cos()], -1)
42
+
43
+
44
+ def body_rope(q, k, gh, gw, base: float = 100.0):
45
+ """2D RoPE: 4 четверти head_dim, две крутятся по x, две по y. Фаза — в латентных пикселях."""
46
+ key = (gh, gw, q.shape[-1], q.device, base)
47
+ tabs = _ROPE_CACHE.get(key)
48
+ if tabs is None:
49
+ dq = q.shape[-1] // 4
50
+ fr = torch.exp(-math.log(base) * torch.arange(dq, device=q.device, dtype=torch.float32) / dq)
51
+ yy = torch.arange(gh, device=q.device).repeat_interleave(gw).float()
52
+ xx = torch.arange(gw, device=q.device).repeat(gh).float()
53
+ ax, ay = xx[:, None] * fr, yy[:, None] * fr
54
+ tabs = (ax.cos()[None, None], ax.sin()[None, None], ay.cos()[None, None], ay.sin()[None, None])
55
+ _ROPE_CACHE[key] = tabs
56
+ cx, sx, cy, sy = tabs
57
+ dq = q.shape[-1] // 4
58
+
59
+ def rot(t):
60
+ a, b, c, e = t[..., :dq], t[..., dq:2 * dq], t[..., 2 * dq:3 * dq], t[..., 3 * dq:]
61
+ return torch.cat([a * cx - b * sx, a * sx + b * cx, c * cy - e * sy, c * sy + e * cy], dim=-1)
62
+
63
+ return rot(q), rot(k)
64
+
65
+
66
+ class RMSNorm(nn.Module):
67
+ def __init__(self, d: int, eps: float = 1e-6):
68
+ super().__init__()
69
+ self.d, self.eps = d, eps
70
+ self.scale = nn.Parameter(torch.zeros(d))
71
+
72
+ def forward(self, x):
73
+ return F.rms_norm(x.float(), (self.d,), weight=(self.scale.float() + 1.0), eps=self.eps).to(x.dtype)
74
+
75
+
76
+ class ConvBlock(nn.Module):
77
+ """ConvNeXt-ish: RMSNorm → depthwise 3×3 → expand 4× → GELU → project, всё под adaLN-гейтом."""
78
+
79
+ def __init__(self, c: int, expand: int = 4, text_dim: int | None = None, heads: int = 6):
80
+ super().__init__()
81
+ self.c, self.hid, self.h, self.hd = c, c * expand, heads, c // heads
82
+ self.norm = RMSNorm(c)
83
+ self.norm_x = RMSNorm(c)
84
+ self.dw = nn.Conv2d(c, c, 3, padding=1, groups=c)
85
+ self.up = nn.Linear(c, self.hid)
86
+ self.down = nn.Linear(self.hid, c)
87
+ self.mod = nn.Parameter(torch.zeros(5, c)) # shift, scale, gate | shift_x, scale_x
88
+ self.xatt = text_dim is not None
89
+ if self.xatt: # текст на мелком уровне: не экономим
90
+ self.xq, self.xo = nn.Linear(c, c, bias=False), nn.Linear(c, c, bias=False)
91
+ nn.init.zeros_(self.down.weight); nn.init.zeros_(self.down.bias)
92
+
93
+ def forward(self, x, vec, gh, gw, text=None, text_mask=None):
94
+ B = x.shape[0]
95
+ shift, scale, gate, shift_x, scale_x = (self.mod + vec).unbind(1)
96
+ h = self.norm(x) * (1 + scale[:, None]) + shift[:, None]
97
+ h = self.dw(h.transpose(1, 2).reshape(B, self.c, gh, gw)).flatten(2).transpose(1, 2)
98
+ x = x + gate[:, None] * self.down(F.gelu(self.up(h)))
99
+ if self.xatt and text is not None:
100
+ N = x.shape[1]
101
+ qq = self.xq(self.norm_x(x) * (1 + scale_x[:, None]) + shift_x[:, None])
102
+ qq = qq.reshape(B, N, self.h, self.hd).transpose(1, 2)
103
+ kk, vv = text
104
+ mask = text_mask[:, None, None, :] if text_mask is not None else None
105
+ xa = F.scaled_dot_product_attention(qq, kk, vv, attn_mask=mask)
106
+ x = x + self.xo(xa.transpose(1, 2).reshape(B, N, self.c))
107
+ return x
108
+
109
+
110
+ class AttnBlock(nn.Module):
111
+ """Pre-norm трансформер: self-attention с 2D RoPE + (опц.) cross-attn к тексту + FFN."""
112
+
113
+ def __init__(self, c: int, heads: int, xatt_dim: int | None, mlp: float = 2.0):
114
+ super().__init__()
115
+ self.c, self.h, self.hd = c, heads, c // heads
116
+ self.n1, self.n2, self.n3 = RMSNorm(c), RMSNorm(c), RMSNorm(c)
117
+ self.qkv = nn.Linear(c, 3 * c, bias=False)
118
+ self.o = nn.Linear(c, c, bias=False)
119
+ self.up, self.down = nn.Linear(c, int(c * mlp)), nn.Linear(int(c * mlp), c)
120
+ self.mod = nn.Parameter(torch.zeros(6, c))
121
+ self.xatt = xatt_dim is not None
122
+ if self.xatt:
123
+ self.xq, self.xo = nn.Linear(c, c, bias=False), nn.Linear(c, c, bias=False)
124
+ nn.init.zeros_(self.down.weight); nn.init.zeros_(self.down.bias)
125
+
126
+ def forward(self, x, vec, gh, gw, text=None, text_mask=None):
127
+ B, N, C = x.shape
128
+ s1, g1, s2, g2, s3, g3 = (self.mod + vec).unbind(1)
129
+ h = self.n1(x) * (1 + s1[:, None])
130
+ q, k, v = self.qkv(h).reshape(B, N, 3, self.h, self.hd).permute(2, 0, 3, 1, 4)
131
+ q, k = body_rope(q, k, gh, gw)
132
+ a = F.scaled_dot_product_attention(q, k, v).transpose(1, 2).reshape(B, N, C)
133
+ x = x + g1[:, None] * self.o(a)
134
+ if self.xatt and text is not None:
135
+ qq = self.xq(self.n2(x)).reshape(B, N, self.h, self.hd).transpose(1, 2)
136
+ kk, vv = text
137
+ mask = text_mask[:, None, None, :] if text_mask is not None else None
138
+ xa = F.scaled_dot_product_attention(qq, kk, vv, attn_mask=mask)
139
+ x = x + g2[:, None] * self.xo(xa.transpose(1, 2).reshape(B, N, C))
140
+ h = self.n3(x) * (1 + s3[:, None])
141
+ return x + g3[:, None] * self.down(F.gelu(self.up(h)))
142
+
143
+
144
+ def zero_out(blk):
145
+ """Обнулить выходы внимания: новый блок на старте тождественен (FFN-выход и так zero-init)."""
146
+ for name in ("o", "xo"):
147
+ lin = getattr(blk, name, None)
148
+ if isinstance(lin, nn.Linear):
149
+ nn.init.zeros_(lin.weight)
150
+ if lin.bias is not None:
151
+ nn.init.zeros_(lin.bias)
152
+ return blk
153
+
154
+
155
+ GRAFT_DEFAULT = {"e0x": 3, "d0x": 3, "e1": 1, "d1": 1, "e2": 2, "d2": 2, "e3": 2}
156
+
157
+
158
+ def sincos_2d(gh: int, gw: int, dim: int, device, dtype=None):
159
+ """Абсолютные 2D-позиции (DiT-стиль sin/cos по каждой оси) — в паре с RoPE в attention."""
160
+ key = ("pe2d", gh, gw, dim, str(device))
161
+ tab = _ROPE_CACHE.get(key)
162
+ if tab is None:
163
+ half = dim // 2
164
+ f = 1.0 / (10000 ** (torch.arange(half // 2, device=device, dtype=torch.float32) / (half / 2)))
165
+ y = torch.arange(gh, device=device, dtype=torch.float32)[:, None] * f[None]
166
+ x = torch.arange(gw, device=device, dtype=torch.float32)[:, None] * f[None]
167
+ ey = torch.cat([y.sin(), y.cos()], -1)[:, None, :].expand(gh, gw, half)
168
+ ex = torch.cat([x.sin(), x.cos()], -1)[None, :, :].expand(gh, gw, half)
169
+ tab = torch.cat([ey, ex], -1).reshape(gh * gw, dim)
170
+ _ROPE_CACHE[key] = tab
171
+ return tab if dtype is None else tab.to(dtype)
172
+
173
+
174
+ class Body(nn.Module):
175
+ """U-стек: ширина 192 → 512 → 1024 → 1536 с аддитивными скипами.
176
+
177
+ Базовая архитектура = 36 блоков: L0 10 conv + 4 attn, L1 9, L2 9, L3 4 (238.5M, 44.4 GFLOP на 256 px).
178
+ Привитые блоки входят в GRAFT_DEFAULT и на конструкторе тождественны — так старые чекпойнты
179
+ (26 блоков) грузятся без сломки: лишние блоки остаются тождественными.
180
+ """
181
+
182
+ def __init__(self, latent_ch: int = 32, text_dim: int = 768,
183
+ widths: tuple = (192, 512, 1024, 1536), enc: tuple = (6, 4, 3, 2),
184
+ dec: tuple = (4, 3, 2), xatt: bool = True):
185
+ super().__init__()
186
+ w0, w1, w2, w3 = widths
187
+ self.widths, self.enc, self.dec = widths, enc, dec
188
+ self.tdim = text_dim if xatt else None
189
+ heads = {0: 6, 1: 8, 2: 8, 3: 8}
190
+ self.stem = nn.Linear(latent_ch, w0)
191
+ self.pe_dim = 64
192
+ self.pe = nn.Linear(self.pe_dim, w0, bias=False) # zero-init: на старте не вмешивается
193
+ nn.init.zeros_(self.pe.weight)
194
+ self.e0 = nn.ModuleList([ConvBlock(w0, text_dim=text_dim if xatt else None) for _ in range(enc[0])])
195
+ self.e0a = AttnBlock(w0, 6, text_dim if xatt else None)
196
+ self.d0a = AttnBlock(w0, 6, text_dim if xatt else None)
197
+ self.e0x = nn.ModuleList() # привива��мые L0-attn (пусто = архитектура без изменений)
198
+ self.d0x = nn.ModuleList()
199
+ self.e1 = nn.ModuleList([AttnBlock(w1, heads[1], text_dim if xatt else None) for _ in range(enc[1])])
200
+ self.e2 = nn.ModuleList([AttnBlock(w2, heads[2], text_dim if xatt else None) for _ in range(enc[2])])
201
+ self.e3 = nn.ModuleList([AttnBlock(w3, heads[3], text_dim if xatt else None) for _ in range(enc[3])])
202
+ self.d2 = nn.ModuleList([AttnBlock(w2, heads[2], text_dim if xatt else None) for _ in range(dec[2])])
203
+ self.d1 = nn.ModuleList([AttnBlock(w1, heads[1], text_dim if xatt else None) for _ in range(dec[1])])
204
+ self.d0 = nn.ModuleList([ConvBlock(w0, text_dim=text_dim if xatt else None) for _ in range(dec[0])])
205
+ self.dn01, self.dn12, self.dn23 = nn.Linear(w0, w1), nn.Linear(w1, w2), nn.Linear(w2, w3)
206
+ self.up32, self.up21, self.up10 = nn.Linear(w3, w2), nn.Linear(w2, w1), nn.Linear(w1, w0)
207
+ self.tmlp = nn.Sequential(nn.Linear(256, 512), nn.SiLU(), nn.Linear(512, 512))
208
+ self.pmlp = nn.Linear(text_dim, 512)
209
+ self.ada = nn.ModuleDict({f"l{i}": nn.Sequential(nn.SiLU(), nn.Linear(512, (5 if i == 0 else 6) * w))
210
+ for i, w in enumerate(widths)})
211
+ self.ada["l0a"] = nn.Sequential(nn.SiLU(), nn.Linear(512, 6 * w0)) # для self-attn на L0
212
+ self.xk0 = nn.Linear(text_dim, 2 * w0)
213
+ self.xk1, self.xk2, self.xk3 = (nn.Linear(text_dim, 2 * w1), nn.Linear(text_dim, 2 * w2),
214
+ nn.Linear(text_dim, 2 * w3))
215
+ self.out_norm = RMSNorm(w0)
216
+ self.out = nn.Linear(w0, latent_ch)
217
+ nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias)
218
+ self.graft(GRAFT_DEFAULT) # базовая архитектура уже с прививкой (блоки тождественны)
219
+
220
+ # ---- прививка блоков
221
+ def graft(self, plan: dict) -> int:
222
+ """Добавить блоки к уже обученной модели так, чтобы они стартовали тождеством.
223
+
224
+ plan: {"e1": 2, "d1": 2, "e3": 2}; ключи — имена списков блоков.
225
+ Тождество: FFN-выход (`down`) zero-init, выходы внимания (`o`, `xo`) обнуляем здесь.
226
+ """
227
+ heads = {0: 6, 1: 8, 2: 8, 3: 8}
228
+ lvl = {"e0": 0, "d0": 0, "e0x": 0, "d0x": 0, "e1": 1, "d1": 1, "e2": 2, "d2": 2, "e3": 3}
229
+ dev = next(self.parameters()).device
230
+ added = 0
231
+ for key, n in plan.items():
232
+ seq = getattr(self, key)
233
+ for _ in range(int(n)):
234
+ if key in ("e0", "d0"):
235
+ blk = ConvBlock(self.widths[0], text_dim=self.tdim)
236
+ else:
237
+ blk = AttnBlock(self.widths[lvl[key]], heads[lvl[key]], self.tdim)
238
+ seq.append(zero_out(blk).to(dev))
239
+ added += 1
240
+ return added
241
+
242
+ # ---- переходы
243
+ @staticmethod
244
+ def downsample(x, gh, gw, proj):
245
+ B = x.shape[0]
246
+ d = x.view(B, gh // 2, 2, gw // 2, 2, -1).mean(dim=(2, 4))
247
+ return proj(d.reshape(B, (gh // 2) * (gw // 2), -1))
248
+
249
+ @staticmethod
250
+ def upsample(x, gh, gw, proj):
251
+ """(B, gh*gw, C) -> (B, 4*gh*gw, C') в родительском row-major порядке."""
252
+ B = x.shape[0]
253
+ d = proj(x).view(B, gh, 1, gw, 1, -1).expand(B, gh, 2, gw, 2, proj.out_features)
254
+ return d.reshape(B, 4 * gh * gw, proj.out_features)
255
+
256
+ def _txt(self, proj, text, heads):
257
+ B, L, _ = text.shape
258
+ kk, vv = proj(text).chunk(2, dim=-1)
259
+ hd = kk.shape[-1] // heads
260
+ return (kk.reshape(B, L, heads, hd).transpose(1, 2), vv.reshape(B, L, heads, hd).transpose(1, 2))
261
+
262
+ def forward(self, z, t, text, text_mask, gh=None, gw=None):
263
+ """gh/gw принимаются для совместимости с интерфейсом UDM и игнорируются."""
264
+ B, C, gh0, gw0 = z.shape
265
+ # pooled-текст в adaLN: среднее ТОЛЬКО по реальным токенам (паддинг иначе разбавляет вектор,
266
+ # и поведение модели начинает зависеть от длины паддинга, а не от подписи)
267
+ # pooled-текст для adaLN: среднее ТОЛЬКО по реальным токенам. Иначе вектор зависит от
268
+ # длины паддинга, а не от подписи (обучение паддит до длины подписи, инференс — до 192)
269
+ m = text_mask[..., None].to(text.dtype)
270
+ pooled = (text * m).sum(1) / m.sum(1).clamp(min=1.0)
271
+ cond = self.tmlp(t_embed(t, 256)) + self.pmlp(pooled)
272
+ v = [self.ada[f"l{i}"](cond).view(B, 5 if i == 0 else 6, -1) for i in range(4)]
273
+ v0a = self.ada["l0a"](cond).view(B, 6, -1)
274
+ tk0 = self._txt(self.xk0, text, 6)
275
+ tk1 = self._txt(self.xk1, text, 8)
276
+ tk2 = self._txt(self.xk2, text, 8)
277
+ tk3 = self._txt(self.xk3, text, 8)
278
+
279
+ x = self.stem(z.flatten(2).transpose(1, 2))
280
+ x = x + self.pe(sincos_2d(gh0, gw0, self.pe_dim, x.device, x.dtype))
281
+ for blk in self.e0:
282
+ x = blk(x, v[0], gh0, gw0, tk0, text_mask)
283
+ x = self.e0a(x, v0a, gh0, gw0, tk0, text_mask)
284
+ for blk in self.e0x:
285
+ x = blk(x, v0a, gh0, gw0, tk0, text_mask)
286
+ s0 = x
287
+ g1h, g1w = gh0 // 2, gw0 // 2
288
+ x = self.downsample(x, gh0, gw0, self.dn01)
289
+ for blk in self.e1:
290
+ x = blk(x, v[1], g1h, g1w, tk1, text_mask)
291
+ s1 = x
292
+ g2h, g2w = g1h // 2, g1w // 2
293
+ x = self.downsample(x, g1h, g1w, self.dn12)
294
+ for blk in self.e2:
295
+ x = blk(x, v[2], g2h, g2w, tk2, text_mask)
296
+ s2 = x
297
+ g3h, g3w = g2h // 2, g2w // 2
298
+ x = self.downsample(x, g2h, g2w, self.dn23)
299
+ for blk in self.e3:
300
+ x = blk(x, v[3], g3h, g3w, tk3, text_mask)
301
+ # декодер: скипы аддитивные
302
+ x = self.upsample(x, g3h, g3w, self.up32) + s2
303
+ for blk in self.d2:
304
+ x = blk(x, v[2], g2h, g2w, tk2, text_mask)
305
+ x = self.upsample(x, g2h, g2w, self.up21) + s1
306
+ for blk in self.d1:
307
+ x = blk(x, v[1], g1h, g1w, tk1, text_mask)
308
+ x = self.upsample(x, g1h, g1w, self.up10) + s0
309
+ x = self.d0a(x, v0a, gh0, gw0, tk0, text_mask)
310
+ for blk in self.d0x:
311
+ x = blk(x, v0a, gh0, gw0, tk0, text_mask)
312
+ for blk in self.d0:
313
+ x = blk(x, v[0], gh0, gw0, tk0, text_mask)
314
+ return self.out(self.out_norm(x)).transpose(1, 2).reshape(B, C, gh0, gw0)
315
+
316
+
317
+ class UDiT(nn.Module):
318
+ """Пирамидальное тело + хвост-декодер AE; интерфейс как у UDM."""
319
+
320
+ def __init__(self, ae, text_dim: int = 768, **kw):
321
+ super().__init__()
322
+ self.ae = ae
323
+ self.body = Body(latent_ch=ae.config.latent_channels, text_dim=text_dim, **kw)
324
+
325
+ def to_raw(self, z):
326
+ return z * self.ae.latent_std + self.ae.latent_mean
327
+
328
+ @torch.no_grad()
329
+ def sample(self, text, text_mask, gh, gw, steps=4, tail_steps=4, generator=None,
330
+ param="x0", cfg=1.0, null_text=None, null_mask=None, clip_norm=0.0):
331
+ dev = self.ae.device
332
+ z = torch.randn(1, self.ae.config.latent_channels, gh, gw, device=dev, generator=generator)
333
+ with torch.autocast(dev.type, torch.bfloat16, enabled=dev.type == "cuda"):
334
+ for i in range(steps):
335
+ t = torch.full((1,), 1.0 - i / steps, device=dev)
336
+ o = self.body(z, t, text, text_mask).float()
337
+ if cfg > 1.0 and null_text is not None:
338
+ ou = self.body(z, t, null_text, null_mask).float()
339
+ o = ou + cfg * (o - ou)
340
+ v = o if param == "vel" else (z - o) / t.clamp(min=1e-2)[:, None, None, None]
341
+ z = z - v / steps
342
+ if clip_norm:
343
+ z = z.clamp(-clip_norm, clip_norm)
344
+ return self.ae.decode_raw(self.to_raw(z), steps=tail_steps, generator=generator)
src/publish.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", "requirements.txt", "run_sdxs.sh", ".gitattributes", "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
+ ]
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("--readme", default="README.md", help="какой README положить в корень")
53
+ ap.add_argument("--push", action="store_true", help="залить собранное на Hub (публичный репозиторий)")
54
+ ap.add_argument("--step", type=int, default=None, help="шаг чекпойнта — только для сообщения коммита")
55
+ args = ap.parse_args()
56
+ out = Path(args.out)
57
+ if out.exists():
58
+ shutil.rmtree(out)
59
+ out.mkdir(parents=True)
60
+
61
+ rels = list(COPY) + [args.readme, "vae/config.json", "transformer/config.json"]
62
+ for rel in rels:
63
+ src = ROOT / rel
64
+ if not src.exists():
65
+ continue
66
+ dst = out / rel
67
+ dst.parent.mkdir(parents=True, exist_ok=True)
68
+ if src.is_dir():
69
+ shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", ".ipynb_checkpoints"))
70
+ else:
71
+ dst_rel = "README.md" if rel == args.readme else rel
72
+ shutil.copy2(src, (out / dst_rel))
73
+
74
+ tdir, vdir = ROOT / "transformer", ROOT / "vae"
75
+ half_file(tdir / "diffusion_pytorch_model.safetensors",
76
+ out / "transformer" / "diffusion_pytorch_model.safetensors")
77
+ vae_weights = vdir / "model.safetensors"
78
+ if not vae_weights.exists(): # AsymmetricAutoencoderKL кладёт так
79
+ vae_weights = vdir / "diffusion_pytorch_model.safetensors"
80
+ half_file(vae_weights, out / "vae" / vae_weights.name)
81
+
82
+ sd = torch.load(ROOT / "text_encoder" / "pytorch_model.bin", map_location="cpu", weights_only=True)
83
+ sd = {k: v for k, v in sd.items() if not k.startswith(("head.", "decoder."))} # MLM-голова не нужна
84
+ save_file({k: v.half().contiguous() for k, v in sd.items()},
85
+ str(out / "text_encoder" / "model.safetensors"))
86
+ print(f"[fp16] text_encoder/model.safetensors: {len(sd)} тензоров "
87
+ f"({sum(v.numel() for v in sd.values()) / 1e6:.1f}M), "
88
+ f"{(out / 'text_encoder' / 'model.safetensors').stat().st_size / 2**20:.0f} МиБ", flush=True)
89
+
90
+ cfg_path = out / "transformer" / "config.json"
91
+ cfg = json.loads(cfg_path.read_text())
92
+ cfg["storage_dtype"] = "float16"
93
+ cfg_path.write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
94
+
95
+ total = sum(f.stat().st_size for f in out.rglob("*") if f.is_file())
96
+ print(f"[готово] {out}: {total / 2**30:.2f} ГиБ", flush=True)
97
+
98
+ if args.push:
99
+ from huggingface_hub import CommitOperationAdd, HfApi
100
+ api = HfApi()
101
+ api.create_repo(args.repo, repo_type="model", private=False, exist_ok=True)
102
+ # create_commit вместо upload_folder: upload_folder уважает .gitignore из бандла
103
+ # (а там шаблоны `*.safetensors`) и молча не зали��ает веса
104
+ ops = [CommitOperationAdd(path_in_repo=p.relative_to(out).as_posix(), path_or_fileobj=str(p))
105
+ for p in sorted(out.rglob("*")) if p.is_file()]
106
+ api.create_commit(repo_id=args.repo, repo_type="model", operations=ops,
107
+ commit_message=(f"Ra sdxs-VAE (step {args.step})" if args.step
108
+ else "Ra sdxs-VAE (weights)"))
109
+ print(f"[push] https://huggingface.co/{args.repo}: {len(ops)} файлов", flush=True)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
src/sample.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 vae_adapter import VAE # 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("--cfg", type=float, default=2.5)
32
+ ap.add_argument("--seed", type=int, default=0)
33
+ ap.add_argument("--n", type=int, default=1, help="сколько картинок подряд от --seed")
34
+ ap.add_argument("--text-len", type=int, default=192)
35
+ args = ap.parse_args()
36
+
37
+ ae = VAE.from_pretrained(str(ROOT / "vae"), DEV)
38
+ enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len)
39
+ m = UDiT(ae, text_dim=enc.dim).to(DEV)
40
+
41
+ src = Path(args.model)
42
+ if src.is_dir():
43
+ step = load_exported(m, enc, ae, src, DEV).get("step")
44
+ else:
45
+ sd = torch.load(src, map_location=DEV, weights_only=False)
46
+ own = m.body.state_dict()
47
+ m.body.load_state_dict({k: v for k, v in sd["body"].items() if k in own}, strict=False)
48
+ enc.mixer.load_state_dict(sd["mixer"])
49
+ if sd.get("decoder") is not None:
50
+ ae.decoder.load_state_dict(sd["decoder"])
51
+ step = sd.get("step")
52
+ m.eval()
53
+ print(f"[model] {src}" + (f", шаг {step}" if step else ""), flush=True)
54
+
55
+ null_hs, null_mk = enc.slices([""], pad_to_len=args.text_len)
56
+ null_t = enc.mix(null_hs.float())
57
+ hs, mk = enc.slices([args.prompt], pad_to_len=args.text_len)
58
+ txt = enc.mix(hs.float())
59
+
60
+ out = Path(args.out)
61
+ out.parent.mkdir(parents=True, exist_ok=True)
62
+ gh = gw = args.size // 8
63
+ for i in range(args.n):
64
+ y = m.sample(txt, mk, gh, gw, steps=args.steps, tail_steps=0, cfg=args.cfg,
65
+ null_text=null_t, null_mask=null_mk,
66
+ generator=torch.Generator(DEV).manual_seed(args.seed + i))
67
+ im = Image.fromarray(((y[0].clamp(-1, 1) + 1) * 127.5).byte().permute(1, 2, 0).cpu().numpy())
68
+ path = out if args.n == 1 else out.with_name(f"{out.stem}_{args.seed + i}{out.suffix}")
69
+ im.save(path)
70
+ print(f"[ok] {path} {im.size[0]}x{im.size[1]}, seed {args.seed + i}", flush=True)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ 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/tfrecord_stream.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Поток картинок из TFRecord-шардов i1 (PNG 256×256 + 5 подписей) вместо parquet-потока FLUX.
2
+
3
+ Отличие от HFImageStream: запись TFRecord = [uint64 len][crc][payload][crc], внутри PNG-байты
4
+ и `caption` — пять длинных перефразировок одной картинки. Поэтому:
5
+ * из пяти подписей берём случайно самую короткую или следующую по длине (бюджета на все нет,
6
+ а короткая лучше влезает в --text-len);
7
+ * картинки уже 256×256 — ресайз только если сторона не совпала с --size;
8
+ * источник: локальная папка с шардами (быстро) или прямо репозиторий HF через range-запросы
9
+ (block_size 16 МБ даёт ~90 записей/с, ~9.7 МБ/с — на уровне нашего темпа обучения).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import io
14
+ import queue
15
+ import random
16
+ import struct
17
+ import threading
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+ import torch
22
+ from PIL import Image
23
+
24
+ SHARD_GLOB = "*.tfrecord-*"
25
+
26
+
27
+ # ---------------------------------------------------------------- разбор формата
28
+ def _varint(buf: bytes, i: int) -> tuple[int, int]:
29
+ shift = val = 0
30
+ while True:
31
+ b = buf[i]; i += 1
32
+ val |= (b & 0x7F) << shift
33
+ if not b & 0x80:
34
+ return val, i
35
+ shift += 7
36
+
37
+
38
+ def _fields(buf: bytes):
39
+ i = 0
40
+ while i < len(buf):
41
+ key, i = _varint(buf, i)
42
+ num, wire = key >> 3, key & 7
43
+ if wire == 2:
44
+ ln, i = _varint(buf, i)
45
+ yield num, buf[i:i + ln]
46
+ i += ln
47
+ elif wire == 0:
48
+ v, i = _varint(buf, i)
49
+ yield num, v
50
+ elif wire == 1:
51
+ yield num, buf[i:i + 8]; i += 8
52
+ elif wire == 5:
53
+ yield num, buf[i:i + 4]; i += 4
54
+ else:
55
+ raise ValueError(f"wire type {wire}")
56
+
57
+
58
+ def _packed_i64(buf: bytes) -> list[int]:
59
+ out, i = [], 0
60
+ while i < len(buf):
61
+ v, i = _varint(buf, i)
62
+ out.append(v)
63
+ return out
64
+
65
+
66
+ def parse_example(payload: bytes) -> dict:
67
+ """tf.train.Example → {поле: список bytes | список int}"""
68
+ out = {}
69
+ for num, features in _fields(payload):
70
+ if num != 1:
71
+ continue
72
+ for fn, entry in _fields(features):
73
+ if fn != 1:
74
+ continue
75
+ key, val = None, {}
76
+ for en, ev in _fields(entry):
77
+ if en == 1:
78
+ key = ev.decode()
79
+ elif en == 2:
80
+ for fnum, fval in _fields(ev):
81
+ if fnum == 1:
82
+ val["bytes"] = [v for n, v in _fields(fval) if n == 1]
83
+ elif fnum == 3:
84
+ val["int64"] = [x for n, v in _fields(fval) if n == 1 for x in _packed_i64(v)]
85
+ out[key] = val.get("bytes", val.get("int64"))
86
+ return out
87
+
88
+
89
+ def count_records(paths) -> int:
90
+ """Сколько записей в шардах — читаем только заголовки (seek через payload), быстро."""
91
+ total = 0
92
+ for path in paths:
93
+ with open(path, "rb") as fh:
94
+ size = fh.seek(0, 2)
95
+ pos = 0
96
+ while pos + 12 <= size:
97
+ fh.seek(pos)
98
+ head = fh.read(8)
99
+ if len(head) < 8:
100
+ break
101
+ (n,) = struct.unpack("<Q", head)
102
+ pos += 8 + 4 + n + 4
103
+ total += 1
104
+ return total
105
+
106
+
107
+ def count_shard_records(source: str) -> int | None:
108
+ """Точное число записей для локальной папки; None, если источник удалённый."""
109
+ if not Path(source).is_dir():
110
+ return None
111
+ paths = sorted(p.as_posix() for p in Path(source).glob(SHARD_GLOB))
112
+ return count_records(paths) if paths else None
113
+
114
+
115
+ def read_records(fh):
116
+ """файловый объект (локальный или HfFileSystem) → поток payload'ов"""
117
+ while True:
118
+ head = fh.read(8)
119
+ if len(head) < 8:
120
+ return
121
+ (n,) = struct.unpack("<Q", head)
122
+ fh.read(4)
123
+ payload = fh.read(n)
124
+ fh.read(4)
125
+ yield payload
126
+
127
+
128
+ # ---------------------------------------------------------------- поток
129
+ class TFRecordStream:
130
+ def __init__(self, source: str, size: int = 256, workers: int = 8, prefetch: int = 256,
131
+ block_mb: int = 16, seed: int = 0, caption_short2: bool = True):
132
+ """source — локальная папка с шардами или id датасета на HF (repo_type=dataset)."""
133
+ self.q: queue.Queue = queue.Queue(maxsize=prefetch)
134
+ self.stop = False
135
+ self.size, self.workers, self.seed = size, workers, seed
136
+ self.caption_short2 = caption_short2
137
+ self.local = Path(source).is_dir()
138
+ self.source = str(source)
139
+ self.block = block_mb * 2 ** 20
140
+ threading.Thread(target=self._loop, daemon=True).start()
141
+
142
+ # ---- список шардов
143
+ def _shards(self) -> list[str]:
144
+ if self.local:
145
+ files = sorted(p.as_posix() for p in Path(self.source).glob(SHARD_GLOB))
146
+ else:
147
+ from huggingface_hub import HfFileSystem
148
+ fs = HfFileSystem()
149
+ files = [f"datasets/{self.source}/{p}" for p in sorted(fs.ls(f"datasets/{self.source}"))
150
+ if ".tfrecord-" in p]
151
+ if not files:
152
+ raise FileNotFoundError(f"нет шардов в {self.source}")
153
+ random.Random(self.seed).shuffle(files)
154
+ return files
155
+
156
+ def _open(self, path: str):
157
+ if self.local:
158
+ return open(path, "rb")
159
+ from huggingface_hub import HfFileSystem
160
+ return HfFileSystem().open(path, "rb", block_size=self.block)
161
+
162
+ # ---- подготовка одной записи
163
+ def _prepare(self, payload: bytes):
164
+ d = parse_example(payload)
165
+ im = Image.open(io.BytesIO(d["image"][0])).convert("RGB")
166
+ w, h = im.size
167
+ if min(w, h) != self.size:
168
+ sc = self.size / min(w, h)
169
+ im = im.resize((max(self.size, round(w * sc)), max(self.size, round(h * sc))), Image.LANCZOS)
170
+ w, h = im.size
171
+ im = im.crop(((w - self.size) // 2, (h - self.size) // 2,
172
+ (w - self.size) // 2 + self.size, (h - self.size) // 2 + self.size))
173
+ caps = [c.decode() if isinstance(c, bytes) else str(c) for c in d.get("caption", [])]
174
+ caps = sorted(caps, key=len)
175
+ if not caps:
176
+ caps = [""]
177
+ cap = random.choice(caps[:2]) if self.caption_short2 else random.choice(caps)
178
+ arr = torch.from_numpy(np.asarray(im, dtype=np.float32)).permute(2, 0, 1) / 127.5 - 1.0
179
+ return arr, cap
180
+
181
+ def _loop(self):
182
+ import collections
183
+ from concurrent.futures import ThreadPoolExecutor
184
+ with ThreadPoolExecutor(max_workers=self.workers) as pool:
185
+ while not self.stop:
186
+ try:
187
+ for shard in self._shards():
188
+ fh = self._open(shard)
189
+ try:
190
+ it = read_records(fh)
191
+ flying = collections.deque()
192
+ while not self.stop:
193
+ while len(flying) < self.workers:
194
+ flying.append(pool.submit(self._prepare, next(it)))
195
+ self.q.put(flying.popleft().result())
196
+ finally:
197
+ fh.close()
198
+ except Exception as e:
199
+ print(f"[tfrecord] сбой ({type(e).__name__}: {e}), переподключение через 5 с", flush=True)
200
+ import time
201
+ time.sleep(5)
202
+
203
+ def batch(self, n: int):
204
+ imgs, caps = [], []
205
+ while len(imgs) < n:
206
+ arr, cap = self.q.get()
207
+ imgs.append(arr); caps.append(cap)
208
+ return torch.stack(imgs), caps
src/train.py ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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), switch, 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 vae_adapter import VAE # noqa: E402
41
+ from tfrecord_stream import TFRecordStream, count_shard_records # noqa: E402 (шарды i1)
42
+
43
+
44
+ def load_ae(args):
45
+ """VAE проекта: асимметричный, декодер отдаёт 2x. Хвоста и пиксельного лосса нет."""
46
+ return VAE.from_pretrained(str(ROOT / "vae"), DEV)
47
+
48
+ ROOT = Path(__file__).resolve().parent.parent
49
+
50
+
51
+ # ------------------------------------------------------------------ расписание
52
+ def lr_at(step: int, base: float, min_lr: float, warmup: int, period: int, start: int = 0) -> float:
53
+ """Прогрев `warmup` шагов от `start`, затем cosine до `min_lr` за `period` шагов."""
54
+ rel = step - start
55
+ if warmup and rel <= warmup:
56
+ return base * min(1.0, rel / max(warmup, 1))
57
+ if period <= warmup:
58
+ return base
59
+ p = min(1.0, (rel - warmup) / (period - warmup))
60
+ return min_lr + (base - min_lr) * 0.5 * (1 + math.cos(math.pi * p))
61
+
62
+
63
+ class HFImageStream:
64
+ """Поток parquet-шардов HF: декод/ресайз/кроп в фоновых потоках, обучение не ждёт диск.
65
+
66
+ Латент и текст НЕ кэшируются (каждая картинка встречается один раз), поэтому шаг дороже,
67
+ чем на папке: +3-4 мс на картинку у AE и +1.2 мс у Ettin.
68
+ """
69
+
70
+ def __init__(self, dataset, parts, size, fields, lo, hi, shuffle_buffer=1000, prefetch=256,
71
+ workers=8, seed=0):
72
+ import queue
73
+ self.q, self.stop = queue.Queue(maxsize=prefetch), False
74
+ self.size, self.fields, self.lo, self.hi = size, fields, lo, hi
75
+ self.dataset, self.parts = dataset, parts
76
+ self.buffer, self.workers, self.seed = shuffle_buffer, workers, seed
77
+ import threading
78
+ threading.Thread(target=self._loop, daemon=True).start()
79
+
80
+ def _items(self):
81
+ """Шарды всех частей перемешиваются между собой (иначе эпоха идёт блоками по частям)."""
82
+ import random
83
+ from datasets import load_dataset
84
+ from huggingface_hub import list_repo_files
85
+ repo = list_repo_files(self.dataset, repo_type="dataset")
86
+ files = [f for f in repo
87
+ if f.endswith(".parquet") and any(f.startswith(p + "/") for p in self.parts)]
88
+ if not files: # запасной путь, если листинг не отдал файлы
89
+ files = [f"hf://datasets/{self.dataset}/{p}/*.parquet" for p in self.parts]
90
+ else:
91
+ random.Random(self.seed).shuffle(files)
92
+ files = [f"hf://datasets/{self.dataset}/{f}" for f in files]
93
+ print(f"[stream] шардов: {len(files)} из частей {self.parts}", flush=True)
94
+ ds = load_dataset("parquet", data_files=files, split="train", streaming=True)
95
+ return ds.shuffle(buffer_size=self.buffer, seed=self.seed)
96
+
97
+ def _prepare(self, item):
98
+ from PIL import Image
99
+ import numpy as np
100
+ im = item["image"]
101
+ if im.mode != "RGB":
102
+ im = im.convert("RGB")
103
+ w, h = im.size
104
+ sc = self.size / min(w, h)
105
+ if abs(sc - 1.0) > 1e-3:
106
+ im = im.resize((max(self.size, round(w * sc)), max(self.size, round(h * sc))), Image.LANCZOS)
107
+ w, h = im.size
108
+ left, top = (w - self.size) // 2, (h - self.size) // 2
109
+ im = im.crop((left, top, left + self.size, top + self.size))
110
+ arr = torch.from_numpy(np.asarray(im, dtype=np.float32)).permute(2, 0, 1) / 127.5 - 1.0
111
+ return arr, self._caption(item)
112
+
113
+ def _caption(self, item):
114
+ clean = lambda x: (x or "").replace("\n", " ").strip() # noqa: E731
115
+ for f in self.fields:
116
+ t = clean(item.get(f))
117
+ if self.lo <= len(t) <= self.hi:
118
+ return t
119
+ return clean(item.get("caption_detail"))
120
+
121
+ def _loop(self):
122
+ import collections
123
+ from concurrent.futures import ThreadPoolExecutor
124
+ with ThreadPoolExecutor(max_workers=self.workers) as pool:
125
+ while not self.stop:
126
+ try:
127
+ it = iter(self._items())
128
+ flying = collections.deque()
129
+ while not self.stop:
130
+ while len(flying) < self.workers:
131
+ flying.append(pool.submit(self._prepare, next(it)))
132
+ self.q.put(flying.popleft().result())
133
+ except Exception as e:
134
+ print(f"[stream] сбой ({type(e).__name__}: {e}), переподключение через 5 с", flush=True)
135
+ time.sleep(5)
136
+
137
+ def batch(self, n):
138
+ imgs, caps = [], []
139
+ while len(imgs) < n:
140
+ arr, cap = self.q.get()
141
+ imgs.append(arr); caps.append(cap)
142
+ return torch.stack(imgs), caps
143
+
144
+
145
+ def hf_energy(t: torch.Tensor) -> float:
146
+ k = torch.tensor([[0.0, 1.0, 0.0], [1.0, -4.0, 1.0], [0.0, 1.0, 0.0]], device=t.device)
147
+ return F.conv2d(t.float().mean(1, keepdim=True), k.view(1, 1, 3, 3), padding=1).std().item()
148
+
149
+
150
+ def psnr(a, b) -> float:
151
+ return 10 * math.log10(4 / max((a.clamp(-1, 1) - b).pow(2).mean().item(), 1e-12))
152
+
153
+
154
+ def rotate(path: Path, keep: int, pattern: str):
155
+ """Оставить `keep` последних файлов, совпадающих с pattern (по номеру шага в имени)."""
156
+ files = sorted(path.glob(pattern))
157
+ for f in files[:-keep]:
158
+ f.unlink()
159
+
160
+
161
+ def main():
162
+ ap = argparse.ArgumentParser(description="UDiT: обучение на папке пар <имя>.jpg + <имя>.txt")
163
+ ap.add_argument("--data", default=str(Path.home() / "dataset" / "butterflies_256sq" / "train"))
164
+ ap.add_argument("--eval", default=str(Path.home() / "dataset" / "butterflies_256sq" / "eval"))
165
+ ap.add_argument("--limit", type=int, default=0, help="взять только первые N картинок (папка)")
166
+ ap.add_argument("--tfrecord", default="",
167
+ help="папка с TFRecord-шардами i1 или id датасета на HF; приоритетнее --hf-dataset")
168
+ ap.add_argument("--hf-dataset", default="LucasFang/FLUX-Reason-6M",
169
+ help="поток HF; пустая строка = папка --data")
170
+ ap.add_argument("--parts", default="Aesthetics-Part01,Aesthetics-Part02,Imaginative,Text")
171
+ ap.add_argument("--caption-fields", default="caption_entity,caption_composition",
172
+ help="короткие поля подписи по порядку предпочтения (иначе caption_detail)")
173
+ ap.add_argument("--caption-len", default=(40, 400), nargs=2, type=int, metavar=("MIN", "MAX"))
174
+ ap.add_argument("--shuffle-buffer", type=int, default=1000)
175
+ ap.add_argument("--prefetch", type=int, default=256)
176
+ ap.add_argument("--workers", type=int, default=8)
177
+ ap.add_argument("--images-per-epoch", type=int, default=5_890_279,
178
+ help="сколько картинок в эпохе потока (для пересчёта --epochs в шаги)")
179
+ ap.add_argument("--size", type=int, default=256, help="разрешение обучения (кадры квадратные)")
180
+ ap.add_argument("--text-len", type=int, default=192, help="предел длины подписи в токенах")
181
+ ap.add_argument("--epochs", type=float, default=None, help="бюджет в эпохах (приоритетнее --steps)")
182
+ ap.add_argument("--steps", type=int, default=None, help="бюджет в шагах (по умолчанию — до --target)")
183
+ ap.add_argument("--target", type=int, default=708000,
184
+ help="абсолютный шаг финиша, если не заданы --steps/--epochs")
185
+ ap.add_argument("--step", type=int, default=0,
186
+ help="стартовый шаг: от него считается остаток и расписание lr")
187
+ ap.add_argument("--batch", type=int, default=16)
188
+ ap.add_argument("--accum", type=int, default=1, help="накопление: эффективный батч = batch*accum")
189
+ ap.add_argument("--probe", action="store_true", help="подобрать максимальный батч под память и выйти")
190
+ ap.add_argument("--lr", type=float, default=4.5e-5)
191
+ ap.add_argument("--lr-warmup", type=int, default=0, help="шагов прогрева (0 — продолжаем с готовых весов)")
192
+ ap.add_argument("--lr-min", type=float, default=1e-5, help="нижняя точка cosine")
193
+ ap.add_argument("--lr-period", type=int, default=0, help="к какому шагу cosine доходит до --lr-min")
194
+ ap.add_argument("--drop-text", type=float, default=0.1, help="доля строк с пустой подписью (CFG)")
195
+ ap.add_argument("--drop-word", type=float, default=0.05, help="доля токенов подписи, скрытых в строке")
196
+
197
+
198
+ ap.add_argument("--graft", default="", help="привить блоки (стартуют тождеством), например 'e0=2,e1=2,d1=2'")
199
+ ap.add_argument("--cfg", type=float, default=2.5, help="CFG при показе сэмплов")
200
+ ap.add_argument("--sample-steps", type=int, default=16, help="шагов тела при сэмплинге")
201
+ ap.add_argument("--init", default=str(ROOT / "transformer"),
202
+ help="стартовые веса: чекпойнт или папка (по умолчанию transformer/)")
203
+ ap.add_argument("--resume", action="store_true", help="продолжить с <out>/last.pt")
204
+ ap.add_argument("--out", default=str(ROOT / "tmp" / "run"))
205
+ ap.add_argument("--log-every", type=int, default=50)
206
+ ap.add_argument("--sample-every", type=int, default=2000, help="0 = не сэмплировать")
207
+ ap.add_argument("--save-every", type=int, default=4000, help="0 = только в конце")
208
+ ap.add_argument("--keep", type=int, default=2, help="сколько чекпойнтов держать")
209
+ ap.add_argument("--samples-keep", type=int, default=4, help="сколько сеток сэмплов держать")
210
+ ap.add_argument("--ema-decay", type=float, default=0.995, help="сглаживание печатаемого лосса")
211
+ ap.add_argument("--ema", type=float, default=0.999, help="EMA весов для сэмплинга (0 = выкл)")
212
+ ap.add_argument("--seed", type=int, default=None)
213
+ args = ap.parse_args()
214
+
215
+ seed = args.seed if args.seed is not None else random.SystemRandom().randrange(2 ** 31)
216
+ torch.manual_seed(seed)
217
+ print(f"[seed] {seed}", flush=True)
218
+
219
+ out = Path(args.out); (out / "samples").mkdir(parents=True, exist_ok=True)
220
+ (out / "ckpt").mkdir(exist_ok=True)
221
+ log_path = out / "log.csv"
222
+ if not (args.resume and log_path.exists()):
223
+ log_path.write_text("step,mse_lat,low,high,mse_pix,switch,bright,contrast,lat_pc_min,"
224
+ "hf_gen,tail_z0,img_s,eta_h,sec\n")
225
+
226
+ from PIL import Image
227
+ import numpy as np
228
+
229
+ def load(path: Path):
230
+ im = Image.open(path).convert("RGB")
231
+ return torch.from_numpy(np.asarray(im, dtype=np.float32)).permute(2, 0, 1) / 127.5 - 1
232
+
233
+ # ---------------------------------------------------------------- данные
234
+ stream = None
235
+ if args.tfrecord:
236
+ stream = TFRecordStream(args.tfrecord, size=args.size, workers=args.workers,
237
+ prefetch=args.prefetch, seed=seed)
238
+ counted = count_shard_records(args.tfrecord)
239
+ n = counted or args.images_per_epoch # эпоха = картинки, реально лежащие в папке
240
+ per_epoch = max(1, n // (args.batch * args.accum))
241
+ steps = int(round(args.epochs * per_epoch)) if args.epochs else (args.steps or 0)
242
+ print(f"[data] TFRecord {args.tfrecord}: "
243
+ + (f"{n} картинок в папке" if counted else f"~{n} картинок (оценка)")
244
+ + f", эпоха = {per_epoch} шагов, подпись = случайная из двух самых коротких → "
245
+ + (f"бюджет {steps} шагов (~{steps * args.batch / 140 / 3600:.1f} ч при 140 карт/с)"
246
+ if steps else f"цель шаг {args.target}"), flush=True)
247
+ ae = load_ae(args)
248
+ enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len)
249
+ HS = MK = X = Z = None
250
+ HS_E, MK_E = enc.slices([""], pad_to_len=args.text_len)
251
+ HS_E = HS_E.to(torch.bfloat16)
252
+ elif args.hf_dataset:
253
+ stream = HFImageStream(args.hf_dataset, args.parts.split(","), args.size,
254
+ args.caption_fields.split(","), args.caption_len[0], args.caption_len[1],
255
+ args.shuffle_buffer, args.prefetch, args.workers, seed)
256
+ n = args.images_per_epoch
257
+ per_epoch = max(1, n // (args.batch * args.accum))
258
+ steps = int(round(args.epochs * per_epoch)) if args.epochs else (args.steps or 0)
259
+ print(f"[data] поток {args.hf_dataset} [{args.parts}], ~{n} картинок/эпоха, "
260
+ f"{per_epoch} шагов/эпоха → "
261
+ + (f"бюджет {steps} шагов (~{steps * args.batch / 62 / 3600:.1f} ч при 62 картинках/с)"
262
+ if steps else f"цель шаг {args.target}"), flush=True)
263
+ ae = load_ae(args)
264
+ enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len)
265
+ HS = MK = X = Z = None
266
+ HS_E, MK_E = enc.slices([""], pad_to_len=args.text_len)
267
+ HS_E = HS_E.to(torch.bfloat16)
268
+ else:
269
+ files = sorted(Path(args.data).glob("*.jpg")) + sorted(Path(args.data).glob("*.png"))
270
+ if args.limit:
271
+ files = files[: args.limit]
272
+ caps = [f.with_suffix(".txt").read_text().strip() for f in files]
273
+ ae = load_ae(args)
274
+ enc = EttinText(str(ROOT / "text_encoder"), DEV, max_len=args.text_len)
275
+ X = torch.stack([load(f) for f in files]).to(DEV)
276
+ with torch.no_grad():
277
+ Z = torch.cat([ae.encode(X[i:i + 8]) for i in range(0, len(X), 8)])
278
+ HS, MK = enc.slices(caps)
279
+ HS, MK = HS.to(torch.bfloat16), MK
280
+ HS_E, MK_E = enc.slices([""], pad_to_len=HS.shape[1])
281
+ HS_E = HS_E.to(torch.bfloat16)
282
+ n = len(files)
283
+ per_epoch = max(1, n // (args.batch * args.accum))
284
+ steps = int(round(args.epochs * per_epoch)) if args.epochs else (args.steps or 0)
285
+ print(f"[data] {n} пар, латент {tuple(Z.shape[1:])}, текст {tuple(HS.shape[1:])}, "
286
+ f"эпоха = {per_epoch} шагов → "
287
+ + (f"бюджет {steps}" if steps else f"цель шаг {args.target}"), flush=True)
288
+
289
+ # ---------------------------------------------------------------- модель и оптимизатор
290
+ m = UDiT(ae, text_dim=enc.dim).to(DEV)
291
+ start = 0
292
+
293
+ def load_body(sd):
294
+ own = m.body.state_dict()
295
+ take = {k: v for k, v in sd.items() if k in own and own[k].shape == v.shape}
296
+ m.body.load_state_dict(take, strict=False)
297
+ return len(take)
298
+
299
+ if args.graft:
300
+ plan = {k.strip(): int(v) for k, v in (kv.split("=") for kv in args.graft.split(","))}
301
+ added = m.body.graft(plan)
302
+ print(f"[graft] привито {added} блоков: {plan}", flush=True)
303
+
304
+ if args.init:
305
+ p = Path(args.init)
306
+ if p.is_dir():
307
+ from export import load_exported
308
+ load_exported(m, enc, ae, p, DEV)
309
+ start = args.step # шаг — параметр запуска, не поле конфига
310
+ print(f"[init] папка {p}; старт шага {start} (--step)", flush=True)
311
+ else:
312
+ sd = torch.load(p, map_location=DEV)
313
+ n_take = load_body(sd["body"]); enc.mixer.load_state_dict(sd["mixer"])
314
+ start = int(sd.get("step", 0))
315
+ print(f"[init] {p}: перенесено {n_take} тензоров, шаг {start}", flush=True)
316
+ if args.resume and (out / "last.pt").exists():
317
+ sd = torch.load(out / "last.pt", map_location=DEV)
318
+ load_body(sd["body"]); enc.mixer.load_state_dict(sd["mixer"])
319
+ start = int(sd.get("step", 0))
320
+ print(f"[resume] {out / 'last.pt'} → шаг {start}", flush=True)
321
+
322
+ if not args.steps and not args.epochs: # бюджет по умолчанию — до абсолютного шага
323
+ steps = max(0, args.target - start)
324
+ print(f"[budget] цель шаг {args.target} от {start} → {steps} шагов", flush=True)
325
+ if steps <= 0:
326
+ print(f"[budget] цель {args.target} уже достигнута (шаг {start}) — учить нечего", flush=True)
327
+ if stream is not None:
328
+ stream.stop = True
329
+ import os as _os
330
+ _os._exit(0)
331
+
332
+ n_body = sum(p.numel() for p in m.body.parameters())
333
+ print(f"[model] тело {n_body/1e6:.1f}M + миксер {sum(p.numel() for p in enc.trainable())/1e6:.2f}M",
334
+ flush=True)
335
+
336
+ groups = [{"params": list(m.body.parameters()), "lr": args.lr},
337
+ {"params": enc.trainable(), "lr": args.lr}]
338
+ params = [p for g in groups for p in g["params"]]
339
+ try:
340
+ import bitsandbytes as bnb
341
+ opt = bnb.optim.AdamW8bit(groups, betas=(0.9, 0.95), weight_decay=0.0)
342
+ print("[opt] AdamW8bit", flush=True)
343
+ except Exception as e:
344
+ opt = torch.optim.AdamW(groups, betas=(0.9, 0.95), weight_decay=0.0)
345
+ print(f"[opt] AdamW fp32 (8bit недоступен: {type(e).__name__})", flush=True)
346
+
347
+ # ---- EMA весов: сглаженные копии для сэмплинга (обучение идёт на сырых)
348
+ ema_body = ema_mixer = None
349
+ if args.ema:
350
+ ema_body = {k: p.detach().float().clone() for k, p in m.body.named_parameters()}
351
+ ema_mixer = {k: p.detach().float().clone() for k, p in enc.mixer.named_parameters()}
352
+ print(f"[ema] decay {args.ema}, {sum(v.numel() for v in ema_body.values())/1e6:.1f}M параметров тела", flush=True)
353
+
354
+ def ema_update():
355
+ if ema_body is None:
356
+ return
357
+ with torch.no_grad():
358
+ for k, p in m.body.named_parameters():
359
+ ema_body[k].mul_(args.ema).add_(p.detach().float(), alpha=1 - args.ema)
360
+ for k, p in enc.mixer.named_parameters():
361
+ ema_mixer[k].mul_(args.ema).add_(p.detach().float(), alpha=1 - args.ema)
362
+
363
+ class _EmaSwap:
364
+ """Подменить веса на EMA на время сэмплинга, потом вернуть сырые."""
365
+ def __enter__(self):
366
+ if ema_body is None:
367
+ return self
368
+ self.raw = {k: p.detach().cpu().clone() for k, p in m.body.named_parameters()}
369
+ self.raw_mx = {k: p.detach().cpu().clone() for k, p in enc.mixer.named_parameters()}
370
+ with torch.no_grad():
371
+ for k, p in m.body.named_parameters():
372
+ p.copy_(ema_body[k].to(p.device))
373
+ for k, p in enc.mixer.named_parameters():
374
+ p.copy_(ema_mixer[k].to(p.device))
375
+ return self
376
+ def __exit__(self, *exc):
377
+ if ema_body is None:
378
+ return False
379
+ with torch.no_grad():
380
+ for k, p in m.body.named_parameters():
381
+ p.copy_(self.raw[k].to(p.device))
382
+ for k, p in enc.mixer.named_parameters():
383
+ p.copy_(self.raw_mx[k].to(p.device))
384
+ return False
385
+
386
+ lr_min = args.lr_min if args.lr_min is not None else args.lr * 0.1
387
+ period = args.lr_period or steps # длина cosine в шагах, отсчёт от start
388
+ print(f"[lr] {args.lr:g}: прогрев {args.lr_warmup} от шага {start}, cosine до {lr_min:g} к шагу {start + period}", flush=True)
389
+
390
+ # ---------------------------------------------------------------- показ сэмплов
391
+ SAMPLE_PROMPTS = [
392
+ "A photograph of a red fox sleeping on a mossy log in a misty forest at dawn.",
393
+ "A city made of glass where rivers of light flow between the towers, cinematic lighting.",
394
+ 'A neon sign reading "hello world" mounted on a brick wall at night in the rain.',
395
+ "An impressionist oil painting of a woman with a parasol standing by a lake.",
396
+ ]
397
+ show_slices, show_x = [], [] # срезы замороженного энкодера; миксер учится — смешиваем при каждом показе
398
+ if stream is not None:
399
+ for cap in SAMPLE_PROMPTS:
400
+ show_slices.append(enc.slices([cap]))
401
+ else:
402
+ ev_all = sorted(Path(args.eval).glob("*.jpg")) + sorted(Path(args.eval).glob("*.png"))
403
+ ev_cls = {}
404
+ for f in ev_all:
405
+ ev_cls.setdefault(f.stem.split("_")[0], f)
406
+ show = [ev_cls[k] for k in list(ev_cls)[:4]]
407
+ if len(show) < 4: # один класс в eval (бабочки) — берём первые 4 файла как есть
408
+ show = ev_all[:4]
409
+ for f in show:
410
+ hs, mk = enc.slices([f.with_suffix(".txt").read_text().strip()], pad_to_len=HS.shape[1])
411
+ show_slices.append((hs, mk))
412
+ show_x.append(load(f)[None].to(DEV))
413
+
414
+ def sample_grid(step):
415
+ tiles, hfs, ys = [], [], []
416
+ # веса уже подменены на EMA в вызывающем месте
417
+ for hs, mk in show_slices:
418
+ y = m.sample(enc.mix(hs.float()), mk, lat_hw[0], lat_hw[1], steps=args.sample_steps,
419
+ tail_steps=0, cfg=args.cfg,
420
+ null_text=enc.mix(HS_E.float()), null_mask=MK_E,
421
+ generator=torch.Generator(DEV).manual_seed(0))
422
+ tiles.append(Image.fromarray(((y[0].clamp(-1, 1) + 1) * 127.5).byte()
423
+ .permute(1, 2, 0).cpu().numpy()))
424
+ hfs.append(hf_energy(y)); ys.append(y)
425
+ if show_x:
426
+ ae_rec = ae.decode_raw(ae.encode(show_x[0]) * ae.latent_std + ae.latent_mean)
427
+ hf_ae = hf_energy(ae_rec)
428
+ else:
429
+ hf_ae = float("nan")
430
+ W, H = tiles[0].size
431
+ canvas = Image.new("RGB", (W * len(tiles), H), "white")
432
+ for i, t in enumerate(tiles):
433
+ canvas.paste(t, (i * W, 0))
434
+ canvas.save(out / "samples" / "latest.png")
435
+ canvas.save(out / "samples" / f"step_{step:07d}.png")
436
+ rotate(out / "samples", args.samples_keep, "step_*.png")
437
+ bright = float(torch.stack(ys).mean())
438
+ contrast = float(torch.stack(ys).std())
439
+ return hfs[0], hf_ae, canvas, bright, contrast
440
+
441
+ def save(step):
442
+ sd = {"body": {k: v.cpu() for k, v in m.body.state_dict().items()},
443
+ "body_ema": ({k: v.cpu() for k, v in ema_body.items()} if ema_body else None),
444
+ "mixer_ema": ({k: v.cpu() for k, v in ema_mixer.items()} if ema_mixer else None),
445
+ "mixer": {k: v.cpu() for k, v in enc.mixer.state_dict().items()},
446
+ "step": step, "args": vars(args), "seed": seed}
447
+ torch.save(sd, out / "last.pt")
448
+ torch.save(sd, out / "ckpt" / f"step_{step:07d}.pt")
449
+ rotate(out / "ckpt", args.keep, "step_*.pt")
450
+
451
+ # ---------------------------------------------------------------- цикл
452
+ lat_hw = (args.size // 8, args.size // 8)
453
+ if args.probe:
454
+ best = args.batch
455
+ for bs in (args.batch, args.batch * 2, args.batch * 4, args.batch * 8):
456
+ try:
457
+ z = torch.randn(bs, 32, lat_hw[0], lat_hw[1], device=DEV)
458
+ t = torch.rand(bs, device=DEV)
459
+ if stream is not None: # в потоке текста нет — берём фиктивный
460
+ hs_probe, mk_probe = enc.slices(["probe caption"] * bs)
461
+ else:
462
+ hs_probe, mk_probe = HS[:bs], MK[:bs]
463
+ with torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"):
464
+ m.body(z, t, enc.mix(hs_probe.float()), mk_probe).float().pow(2).mean().backward()
465
+ best = bs
466
+ m.zero_grad(set_to_none=True)
467
+ except torch.OutOfMemoryError:
468
+ m.zero_grad(set_to_none=True); torch.cuda.empty_cache(); break
469
+ print(f"[probe] максимальный батч ≈ {best} (пик {torch.cuda.max_memory_allocated()/1e9:.1f} ГБ)",
470
+ flush=True)
471
+ if stream is not None:
472
+ stream.stop = True
473
+ import os as _os
474
+ _os._exit(0)
475
+
476
+ if stream is not None:
477
+ def get_batch():
478
+ x, caps = stream.batch(args.batch)
479
+ x = x.to(DEV)
480
+ with torch.no_grad():
481
+ z = ae.encode(x)
482
+ hs, mk = enc.slices(caps)
483
+ return x, z, hs.to(torch.bfloat16), mk
484
+ else:
485
+ def get_batch():
486
+ idx = torch.randint(0, n, (args.batch,), device=DEV)
487
+ return X[idx], Z[idx], HS[idx], MK[idx]
488
+
489
+ hf_gen, hf_ae, _, bright0, contrast0 = sample_grid(start)
490
+ print(f"[sample] шаг {start}: яркость {bright0:+.3f}, контраст {contrast0:.3f}, hf {hf_gen:.3f}", flush=True)
491
+
492
+ t0 = time.perf_counter(); prev_el = t0
493
+ ema = {}
494
+ run = {"lat": 0.0, "low": 0.0, "high": 0.0, "pix": 0.0, "n_low": 0, "n_high": 0, "n": 0}
495
+ skips = 0
496
+ for step in range(start + 1, start + steps + 1):
497
+ opt.zero_grad(set_to_none=True)
498
+ ok = 0
499
+ for _ in range(args.accum):
500
+ x0, z0, hs, mk = get_batch()
501
+ # дропаут подписи (CFG) и дропаут слов внутри подписи
502
+ if args.drop_text:
503
+ drop = torch.rand(args.batch, device=DEV) < args.drop_text
504
+ if drop.any(): # пустая подпись обрезается под длину батча
505
+ L = hs.shape[1]
506
+ hs = torch.where(drop[:, None, None, None], HS_E[:, :L].expand_as(hs), hs)
507
+ mk = torch.where(drop[:, None], MK_E[:, :L].expand_as(mk), mk)
508
+ if args.drop_word:
509
+ word = (torch.rand(mk.shape, device=DEV) < args.drop_word) & mk
510
+ hs = hs.masked_fill(word[:, :, None, None], 0)
511
+ mk = mk & ~word
512
+ with torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"):
513
+ txt = enc.mix(hs.float())
514
+ tb = torch.rand(args.batch, device=DEV)
515
+ eb = torch.randn_like(z0)
516
+ zt = (1 - tb[:, None, None, None]) * z0 + tb[:, None, None, None] * eb
517
+ z_hat = m.body(zt, tb, txt, mk)
518
+ per_elem = (z_hat.float() - z0.float()).pow(2).mean(dim=(1, 2, 3))
519
+ low, high = tb < 0.3, tb > 0.7
520
+ loss_l = per_elem.mean()
521
+ total = loss_l
522
+ if not torch.isfinite(total):
523
+ skips += 1
524
+ continue
525
+ (total / args.accum).backward()
526
+ ok += 1
527
+ run["lat"] += loss_l.item()
528
+ if low.any():
529
+ run["low"] += per_elem[low].mean().item(); run["n_low"] += 1
530
+ if high.any():
531
+ run["high"] += per_elem[high].mean().item(); run["n_high"] += 1
532
+ if not ok:
533
+ print(f" ! шаг {step}: не-конечный лосс, пропуск (всего {skips})", flush=True)
534
+ continue
535
+ run["n"] += 1
536
+ torch.nn.utils.clip_grad_norm_(params, 1.0)
537
+ lr = lr_at(step, args.lr, lr_min, args.lr_warmup, period, start)
538
+ for g, base in zip(opt.param_groups, (args.lr, args.lr)):
539
+ g["lr"] = lr * (base / args.lr)
540
+ opt.step()
541
+ ema_update()
542
+
543
+ if step % args.log_every == 0:
544
+ d = args.ema_decay
545
+ cur = {"lat": run["lat"] / run["n"],
546
+ "low": run["low"] / max(run["n_low"], 1), "high": run["high"] / max(run["n_high"], 1)}
547
+ for k, v in cur.items():
548
+ ema[k] = v if k not in ema else d * ema[k] + (1 - d) * v
549
+ el = time.perf_counter() - t0; inc = el - prev_el; prev_el = el
550
+ ms = inc / max(run["n"], 1) * 1e3
551
+ avg = el / max(step - start, 1) # с/шаг с начала прогона (включая простои)
552
+ left = (start + steps - step) * avg
553
+ eta = time.strftime("%d.%m %H:%M", time.gmtime(time.time() + left))
554
+ print(f" {step:6d} | mse_lat {cur['lat']:.4f} (ema {ema['lat']:.4f})"
555
+ f" | lr {lr:.1e} | {args.batch * args.accum / (ms / 1e3):.1f} карт/с ({ms:.0f} мс/шаг)"
556
+ f" | осталось {left / 3600:.1f} ч (финиш {eta} UTC)"
557
+ + (f" | skip {skips}" if skips else ""), flush=True)
558
+ run = {"lat": 0.0, "low": 0.0, "high": 0.0, "pix": 0.0, "n_low": 0, "n_high": 0, "n": 0}
559
+
560
+ if args.sample_every and step % args.sample_every == 0:
561
+ ema_ctx = _EmaSwap().__enter__()
562
+ if DEV == "cuda":
563
+ torch.cuda.empty_cache() # сэмплинг идёт поверх фрагментированного кэша — чистим заранее
564
+ zs = []
565
+ lat_pc_min = float("nan")
566
+ for hs, mk in show_slices[:2]:
567
+ txt = enc.mix(hs.float())
568
+ z = torch.randn(1, 32, lat_hw[0], lat_hw[1], device=DEV,
569
+ generator=torch.Generator(DEV).manual_seed(7))
570
+ with torch.no_grad(), torch.autocast(DEV, torch.bfloat16, enabled=DEV == "cuda"):
571
+ for i in range(args.sample_steps):
572
+ t = torch.full((1,), 1.0 - i / args.sample_steps, device=DEV)
573
+ o = m.body(z, t, txt, mk).float()
574
+ ou = m.body(z, t, enc.mix(HS_E.float()), MK_E).float()
575
+ o = ou + args.cfg * (o - ou)
576
+ z = z - (z - o) / t.clamp(min=1e-2)[:, None, None, None] / args.sample_steps
577
+ zs.append(z)
578
+ switch = (((zs[0] - zs[1]).norm() / zs[0].norm()).item() if len(zs) > 1 else float("nan"))
579
+ if zs:
580
+ lat_pc_min = float(zs[0].std(dim=(0, 2, 3)).min()) # сторож сжатия дисперсии латента
581
+ hfg, hfa, canvas, bright, contrast = sample_grid(step)
582
+ tail_psnr = float("nan") # метрика хвоста больше не существует
583
+ el = time.perf_counter() - t0
584
+ avg = el / max(step - start, 1); left = (start + steps - step) * avg
585
+ print(f" ↳ {step} | hf {hfg:.3f} | латент std {lat_pc_min:.2f} | switch {switch:.3f}"
586
+ f" | {time.strftime('%H:%M', time.gmtime())} UTC", flush=True)
587
+ with open(log_path, "a") as fh:
588
+ fh.write(f"{step},{ema.get('lat', 0):.5f},{ema.get('low', 0):.5f},{ema.get('high', 0):.5f},"
589
+ f"0.0000,{switch:.4f},{bright:.4f},{contrast:.4f},"
590
+ f"{lat_pc_min:.4f},{hfg:.4f},{tail_psnr:.2f},"
591
+ f"{args.batch * args.accum / (el / max(step - start, 1)):.1f},{left / 3600:.2f},{el:.1f}\n")
592
+ ema_ctx.__exit__(None, None, None) # вернуть сырые веса
593
+ if args.save_every and step % args.save_every == 0:
594
+ save(step)
595
+ save(start + steps)
596
+ if stream is not None:
597
+ stream.stop = True # фоновые потоки не должны переживать интерпретатор
598
+ print("[done]", flush=True)
599
+ import os as _os
600
+ _os._exit(0) # обойти шумную финализацию streaming-генераторов
601
+
602
+
603
+ if __name__ == "__main__":
604
+ main()
src/vae_adapter.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VAE проекта: AsymmetricAutoencoderKL (энкодер f8, декодер f16 → выход 2×).
2
+
3
+ Что адаптер прячет от остального кода:
4
+ * латент нормируется своими `latents_mean`/`latents_std` из vae/config.json (≈ -0.04 / 1.58):
5
+ encode(x) → нормированный латент, `to_raw()` в модели возвращает сырой;
6
+ * декодер асимметричный: энкодер f8, декодер f16, то есть латент 32×32 отдаёт картинку 512²
7
+ (вдвое больше входа) — это и есть смысл этого VAE;
8
+ * декодирование однопроходное — обучение идёт как обычная латентная диффузия (только MSE по латенту).
9
+
10
+ API, которого ждут train.py/sample.py: `encode`, `decode_raw`, `latent_mean`, `latent_std`,
11
+ `config.latent_channels`, `config.patch_size`, `compute_dtype`, `device`, `.eval()`, `.to(device)`.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+
18
+ PATCH = 8 # энкодер сжимает в 8 раз; декодер потом растягивает в 16 → 2× на выходе
19
+
20
+
21
+ class VAE(nn.Module):
22
+ def __init__(self, vae, device: str = "cuda"):
23
+ super().__init__()
24
+ self.vae = vae
25
+ self.device_name = device
26
+ self.compute_dtype = torch.float32
27
+ cfg = vae.config
28
+ mean = torch.tensor(getattr(cfg, "latents_mean"), dtype=torch.float32)
29
+ std = torch.tensor(getattr(cfg, "latents_std"), dtype=torch.float32)
30
+ self.register_buffer("latent_mean", mean.view(1, -1, 1, 1))
31
+ self.register_buffer("latent_std", std.view(1, -1, 1, 1))
32
+ self.config = type("Cfg", (), {"latent_channels": int(cfg.latent_channels),
33
+ "patch_size": PATCH})()
34
+
35
+ @classmethod
36
+ def from_pretrained(cls, path, device: str = "cuda"):
37
+ from diffusers import AsymmetricAutoencoderKL
38
+ vae = AsymmetricAutoencoderKL.from_pretrained(str(path), torch_dtype=torch.float32)
39
+ vae = vae.to(device).eval()
40
+ vae.requires_grad_(False)
41
+ return cls(vae, device).to(device).eval()
42
+
43
+ @property
44
+ def device(self):
45
+ return self.latent_mean.device
46
+
47
+ def train(self, mode: bool = True):
48
+ super().train(mode)
49
+ self.vae.eval() # VAE заморожен всегда
50
+ return self
51
+
52
+ def encode(self, x: torch.Tensor) -> torch.Tensor:
53
+ """x в -1..1 → нормированный латент (B, 32, H/8, W/8)."""
54
+ with torch.no_grad():
55
+ raw = self.vae.encode(x.to(self.compute_dtype)).latent_dist.mode()
56
+ return ((raw - self.latent_mean) / self.latent_std).to(torch.float32)
57
+
58
+ def decode_raw(self, z_raw: torch.Tensor, steps: int = 0, generator=None) -> torch.Tensor:
59
+ """Сырой латент → картинка в -1..1. `steps`/`generator` — для совместимости с нашим API."""
60
+ with torch.no_grad():
61
+ return self.vae.decode(z_raw.to(self.compute_dtype), return_dict=False)[0].clamp(-1, 1)
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,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "UDiT",
3
+ "format": "udm/v1",
4
+ "storage_dtype": "float16",
5
+ "latent_channels": 32,
6
+ "text_dim": 768,
7
+ "body": {
8
+ "kind": "пирамидальный U: ширина растёт к низу",
9
+ "widths": [
10
+ 192,
11
+ 512,
12
+ 1024,
13
+ 1536
14
+ ],
15
+ "heads": [
16
+ 6,
17
+ 8,
18
+ 8,
19
+ 8
20
+ ],
21
+ "blocks": {
22
+ "e0": 6,
23
+ "e0x": 3,
24
+ "d0x": 3,
25
+ "e1": 5,
26
+ "e2": 5,
27
+ "e3": 4,
28
+ "d2": 4,
29
+ "d1": 4,
30
+ "d0": 4
31
+ },
32
+ "levels": [
33
+ {
34
+ "level": 0,
35
+ "tokens": "÷1",
36
+ "width": 192,
37
+ "blocks": "conv 6 enc / 4 dec + 2 self-attn на сторону"
38
+ },
39
+ {
40
+ "level": 1,
41
+ "tokens": "÷2",
42
+ "width": 512,
43
+ "blocks": "attn 5 enc / 4 dec"
44
+ },
45
+ {
46
+ "level": 2,
47
+ "tokens": "÷4",
48
+ "width": 1024,
49
+ "blocks": "attn 5 enc / 4 dec"
50
+ },
51
+ {
52
+ "level": 3,
53
+ "tokens": "÷8",
54
+ "width": 1536,
55
+ "blocks": "attn 4 (боттлнек)"
56
+ }
57
+ ],
58
+ "skips": "аддитивные (SD-стиль): e0/e1/e2 прибавляются в декодере",
59
+ "down": "mean-pool 2x + Linear",
60
+ "up": "Linear + nearest 2x",
61
+ "positions": {
62
+ "rope": {
63
+ "kind": "2d",
64
+ "base": 100,
65
+ "phase": "в латентных пикселях на каждом уровне"
66
+ },
67
+ "absolute": {
68
+ "kind": "2d sincos",
69
+ "dim": 64,
70
+ "into": "zero-init Linear(64 -> width) к выходу stem, в паре с RoPE"
71
+ }
72
+ },
73
+ "text": "кросс-внимание на всех уровнях",
74
+ "parametrization": "x0 латента",
75
+ "params": 240.04
76
+ }
77
+ }
transformer/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0e046b9aaede687fd792d7727ce35ea4287b0c6a8894986f61c5ce370643392d
3
+ size 500218686
vae/config.json ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "AsymmetricAutoencoderKL",
3
+ "_diffusers_version": "0.37.0",
4
+ "_name_or_path": "asymmetric_vae",
5
+ "act_fn": "silu",
6
+ "down_block_out_channels": [
7
+ 128,
8
+ 256,
9
+ 512,
10
+ 512
11
+ ],
12
+ "down_block_types": [
13
+ "DownEncoderBlock2D",
14
+ "DownEncoderBlock2D",
15
+ "DownEncoderBlock2D",
16
+ "DownEncoderBlock2D"
17
+ ],
18
+ "in_channels": 3,
19
+ "latent_channels": 32,
20
+ "latents_mean": [
21
+ -0.03542253375053406,
22
+ 0.20086465775966644,
23
+ -0.016413161531090736,
24
+ -0.0956302210688591,
25
+ -0.2672063112258911,
26
+ 0.2609933018684387,
27
+ -0.07806991040706635,
28
+ -0.48407721519470215,
29
+ 0.21844269335269928,
30
+ -0.1122383326292038,
31
+ 0.27197545766830444,
32
+ -0.18958772718906403,
33
+ 0.18776826560497284,
34
+ 0.0987580344080925,
35
+ 0.2837068736553192,
36
+ -0.4486690163612366,
37
+ 0.4816776514053345,
38
+ 0.02947971224784851,
39
+ -0.1337375044822693,
40
+ -0.39750921726226807,
41
+ -0.08513020724058151,
42
+ -0.054023586213588715,
43
+ -0.3943594992160797,
44
+ 0.23918119072914124,
45
+ -0.12466679513454437,
46
+ 0.09935147315263748,
47
+ 0.31858691573143005,
48
+ 0.48585832118988037,
49
+ -0.6416525840759277,
50
+ -0.15164820849895477,
51
+ -0.4693508744239807,
52
+ -0.13071806728839874
53
+ ],
54
+ "latents_std": [
55
+ 1.5792087316513062,
56
+ 1.5769503116607666,
57
+ 1.5864241123199463,
58
+ 1.6454921960830688,
59
+ 1.5336694717407227,
60
+ 1.5587652921676636,
61
+ 1.5838669538497925,
62
+ 1.5659377574920654,
63
+ 1.6860467195510864,
64
+ 1.5192310810089111,
65
+ 1.573639988899231,
66
+ 1.5953549146652222,
67
+ 1.5271092653274536,
68
+ 1.6246271133422852,
69
+ 1.7054023742675781,
70
+ 1.607722282409668,
71
+ 1.558642864227295,
72
+ 1.5824549198150635,
73
+ 1.6202995777130127,
74
+ 1.6206320524215698,
75
+ 1.6379750967025757,
76
+ 1.6527063846588135,
77
+ 1.498811960220337,
78
+ 1.5706247091293335,
79
+ 1.5854856967926025,
80
+ 1.4828169345855713,
81
+ 1.5693111419677734,
82
+ 1.692481517791748,
83
+ 1.6409776210784912,
84
+ 1.6216280460357666,
85
+ 1.6087706089019775,
86
+ 1.5776633024215698
87
+ ],
88
+ "layers_per_down_block": 2,
89
+ "layers_per_up_block": 2,
90
+ "norm_num_groups": 32,
91
+ "out_channels": 3,
92
+ "sample_size": 32,
93
+ "scaling_factor": 1.0,
94
+ "up_block_out_channels": [
95
+ 128,
96
+ 128,
97
+ 256,
98
+ 512,
99
+ 512
100
+ ],
101
+ "up_block_types": [
102
+ "UpDecoderBlock2D",
103
+ "UpDecoderBlock2D",
104
+ "UpDecoderBlock2D",
105
+ "UpDecoderBlock2D",
106
+ "UpDecoderBlock2D"
107
+ ]
108
+ }
vae/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b11f911599c8f066158206726e7adf8882649e15e6275868f0f435f3b914c00a
3
+ size 191765430