Instructions to use AiArtLab/qwen3-0.6b-4b-adapter with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use AiArtLab/qwen3-0.6b-4b-adapter with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("AiArtLab/qwen3-0.6b-4b-adapter", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
TRAINING.ru.md: подробный русский разбор обучения адаптера
Browse files- почему 6 слоёв ТЕ, а не 3 (нормы 13 -> 557, глубина чтения, совпадение с набором студента)
- почему MLP такой формы, почему поточечности мало и что дала residual-ветка внимания
- лосс, метрика и три группы токенов; почему R² здесь врёт
- данные: баланс по группам + danbooru-теги, пустые промпты, отказ от негативов
- таблицы: версии, лестница 8 эпох, что не сработало, инженерные грабли
- src/export_safetensors.py добавлен, чтобы команды из статьи работали
- README.md +4 -0
- TRAINING.ru.md +357 -0
- src/export_safetensors.py +60 -0
README.md
CHANGED
|
@@ -18,6 +18,8 @@ _Replace klein's 4B text encoder with a 0.6B one — 7.5 GB VRAM and ~0.3 s/step
|
|
| 18 |
|
| 19 |
Train status: trained on one Vast.ai GPU (RTX 5090, 32 GB), text only — no images, no VAE, no diffusion.
|
| 20 |
|
|
|
|
|
|
|
| 21 |
[support train please..](#donations)
|
| 22 |
|
| 23 |
* Adapter: point-wise MLP `6144 -> 8192 -> 8192 -> 7680` (180.4M) **plus a 2-block residual attention branch** (39.6M) — `adapter_v14_bal.safetensors`
|
|
@@ -93,6 +95,8 @@ example.py diffusers usage example
|
|
| 93 |
src/adapter_lib.py adapter schema + loading
|
| 94 |
src/klein_condition.py encode_prompt replacement for the klein pipeline
|
| 95 |
src/train_adapter.py training (online encoders, no cached embeddings)
|
|
|
|
|
|
|
| 96 |
media/before_after.png main illustration: native Qwen3-4B vs Qwen3-0.6B + adapter
|
| 97 |
media/tags_before_after.png danbooru tags and anime-style prompts
|
| 98 |
media/limitations.png known issue: object identity (bulldog)
|
|
|
|
| 18 |
|
| 19 |
Train status: trained on one Vast.ai GPU (RTX 5090, 32 GB), text only — no images, no VAE, no diffusion.
|
| 20 |
|
| 21 |
+
Разбор обучения по-русски, с цифрами и историей решений: [TRAINING.ru.md](TRAINING.ru.md)
|
| 22 |
+
|
| 23 |
[support train please..](#donations)
|
| 24 |
|
| 25 |
* Adapter: point-wise MLP `6144 -> 8192 -> 8192 -> 7680` (180.4M) **plus a 2-block residual attention branch** (39.6M) — `adapter_v14_bal.safetensors`
|
|
|
|
| 95 |
src/adapter_lib.py adapter schema + loading
|
| 96 |
src/klein_condition.py encode_prompt replacement for the klein pipeline
|
| 97 |
src/train_adapter.py training (online encoders, no cached embeddings)
|
| 98 |
+
src/export_safetensors.py .pt -> .safetensors with the schema metadata inside
|
| 99 |
+
TRAINING.ru.md detailed Russian write-up: how it was trained and why
|
| 100 |
media/before_after.png main illustration: native Qwen3-4B vs Qwen3-0.6B + adapter
|
| 101 |
media/tags_before_after.png danbooru tags and anime-style prompts
|
| 102 |
media/limitations.png known issue: object identity (bulldog)
|
TRAINING.ru.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Как обучен адаптер Qwen3-0.6B → Qwen3-4B: подробный разбор
|
| 2 |
+
|
| 3 |
+
Это технический разбор адаптера [`adapter_v14_bal.safetensors`](adapter_v14_bal.safetensors) —
|
| 4 |
+
220M-маппинга из пространства текстового энкодера Qwen3-0.6B в пространство текстового
|
| 5 |
+
энкодера FLUX.2-klein-4B (Qwen3-4B). Английская карточка модели — в [README.md](README.md),
|
| 6 |
+
здесь то же самое, но с цифрами замеров, историей решений и разбором того, что не сработало.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 1. Зачем вообще адаптер
|
| 11 |
+
|
| 12 |
+
Учитель в нашей дистилляции — FLUX.2-klein-4B. Его DiT обучен на текстовом условии, которое
|
| 13 |
+
считает его собственный энкодер: Qwen3-4B, 36 слоёв, hidden 2560, склейка слоёв **9, 18, 27** —
|
| 14 |
+
это `joint_attention_dim = 7680`, и ровно эти три слоя берёт сам пайплайн diffusers.
|
| 15 |
+
|
| 16 |
+
Наш micro-студент имеет дешёвый энкодер — **Qwen3-0.6B** (28 слоёв, hidden 1024), потому что
|
| 17 |
+
в micro-модели весь бюджет уходит в DiT, а не в текст.
|
| 18 |
+
|
| 19 |
+
Возникает развилка при дистилляции:
|
| 20 |
+
|
| 21 |
+
* кормить учителя **его родным** условием (нужен Qwen3-4B: +7.5 ГБ VRAM и ~0.3 с на шаг);
|
| 22 |
+
* либо кормить учителя **нашим** условием, но переведённым в его пространство.
|
| 23 |
+
|
| 24 |
+
Второй путь и есть адаптер: `y = A(наш_ТЕ(x))`, где `A` учится по тексту, без картинок,
|
| 25 |
+
без VAE и без диффузии. Цель — не «похоже на подсказку», а **в точности то, что дал бы родной
|
| 26 |
+
энкодер учителя на том же тексте**. Тогда учитель ведёт студента в том режиме, в каком он
|
| 27 |
+
работал всегда, а студент дистиллируется от условия, которое сам и умеет считать.
|
| 28 |
+
|
| 29 |
+
Ключевое упрощение: задача — чистая регрессия «эмбеддинги в эмбеддинги». Токенизаторы
|
| 30 |
+
у Qwen3-0.6B и Qwen3-4B **идентичны** (vocab 151669, тот же `chat_template`, тот же
|
| 31 |
+
`padding_side=right`), поэтому позиции ученика и учителя совпадают ровно, без выравнивания.
|
| 32 |
+
У референсной работы (`recoilme/sdxs`, `qwen_adapter_2b_4b`) токенизаторы разные, и там
|
| 33 |
+
позиции частично разъезжаются — у нас этой проблемы нет.
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## 2. Как он устроен
|
| 38 |
+
|
| 39 |
+
```
|
| 40 |
+
текст → Qwen3-0.6B (заморожен) → hidden states слоёв 2,9,14,18,23,27 → склейка (B, 256, 6144)
|
| 41 |
+
→ адаптер → (B, 256, 7680) → DiT klein
|
| 42 |
+
|
| 43 |
+
y_i = MLP(x_i) + Attention(x)_i
|
| 44 |
+
│ │
|
| 45 |
+
│ └─ residual-ветка: 2 блока self-attention, d=1024, 8 голов, 39.6M
|
| 46 |
+
└─ поточечная MLP: 6144 → 8192 → 8192 → 7680, GELU(tanh), 180.4M
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
Итого 220.0M параметров. Порядок склейки — послойно-майорный, `[слой2 | слой9 | … | слой27]`,
|
| 50 |
+
ровно так же, как klein собирает свои три слоя (`stack(dim=1)` → `permute`).
|
| 51 |
+
|
| 52 |
+
Применяется адаптер **только к условию учителя**. Наш DiT по-прежнему получает свой 4D-контекст
|
| 53 |
+
`(B, L, 6, 1024)` и обрабатывает его сам (`TextFusion` + `txtmlp`) — адаптер существует для
|
| 54 |
+
взаимодействия с учителем, а не для студента.
|
| 55 |
+
|
| 56 |
+
### Почему шесть слоёв, а не три
|
| 57 |
+
|
| 58 |
+
Соблазн взять симметрию: klein использует три слоя из 36 (25%, 50%, 75%) — значит, и нам взять
|
| 59 |
+
три. Мы так и начали, и это оказалось хуже по всем замерам:
|
| 60 |
+
|
| 61 |
+
| вход адаптера | параметров | отн. ошибка |
|
| 62 |
+
|---|---|---|
|
| 63 |
+
| 3 слоя → 3072, MLP `3072→4096→7680`, полный промпт | 44M | 36.03% |
|
| 64 |
+
| то же, но `--drop-first 5` и 5 датасетов × 3000 | 44M | 32.04% |
|
| 65 |
+
| 3 слоя → 3072, MLP `3072→6144→6144→7680` | 104M | 29.92% |
|
| 66 |
+
| **6 слоёв → 6144, MLP `6144→8192→8192→7680` + `ln-per-layer`** | **180M** | **25.43%** |
|
| 67 |
+
|
| 68 |
+
Причин у выигрыша шесть-слоёв две, и обе измеримые.
|
| 69 |
+
|
| 70 |
+
**Первая — нормы слоёв растут как 13 → 557.** ��орма выхода слоя Qwen3-0.6B по глубине
|
| 71 |
+
отличается в десятки раз. Если склеить слои как есть, вход адаптера почти целиком состоит из
|
| 72 |
+
глубоких слоёв, а ранний слой даёт **0.05%** — то есть его как будто нет. Именно поэтому
|
| 73 |
+
шесть слоёв идут вместе с `--norm ln-per-layer` (у каждого среза свои статистики, без
|
| 74 |
+
обучаемых параметров): после неё все шесть слоёв сопоставимы по масштабу и каждый начинает
|
| 75 |
+
работать. Отдельного замера «шесть слоёв без нормировки» у нас нет — но при таком разбросе
|
| 76 |
+
норм он выродился бы в «три глубоких слоя», то есть в 104M-строку таблицы выше.
|
| 77 |
+
|
| 78 |
+
**Вторая — глубина чтения.** Выход слоя 9 у 36-слойной модели — это абстракция, снятая после
|
| 79 |
+
девяти блоков. У 28-слойной модели такой же по смыслу абстракции соответствует не слой 7
|
| 80 |
+
(та же доля глубины), а другой слой — и «правильная» глубина чтения зависит от токена: для
|
| 81 |
+
поверхностных вещей (регистр, отдельный тег, эмодзи) информативнее ранние слои, для смысла —
|
| 82 |
+
поздние. Отдав адаптеру **несколько** глубин, мы позволяем ему выбирать чтение самому, вместо
|
| 83 |
+
того чтобы угадывать единственный «правильный» слой. Эмпирика это подтверждает: набор
|
| 84 |
+
`2,9,14,18,23,27` — не равномерная сетка (9 и 14 стоят ближе друг к другу), он выбирался
|
| 85 |
+
по замерам, а не по формуле.
|
| 86 |
+
|
| 87 |
+
**Дополнительный аргумент:** ровно этот набор — то, что ест наш micro-DiT
|
| 88 |
+
(`TextFusion` → `txtmlp`). Условие адаптера и условие студента — это одни и те же фичи,
|
| 89 |
+
разница только в цели (пространство 4B против своего). Для дистилляции это удобно: студент
|
| 90 |
+
учится на том же представлении текста, что и потом использует сам.
|
| 91 |
+
|
| 92 |
+
Почему не взять все 28 слоёв: во-первых, вход первого линейного слоя вырос бы до 28 672
|
| 93 |
+
(первый слой стал бы 235M параметров сам по себе), во-вторых, соседние слои сильно
|
| 94 |
+
коррелированы, в-третьих, студенту нужны именно эти шесть. Это выбор, а не измеренный
|
| 95 |
+
оптимум — сравнения «6 против 12 против 28» у нас нет.
|
| 96 |
+
|
| 97 |
+
### Почему MLP, а не что-то другое
|
| 98 |
+
|
| 99 |
+
Рассматривались четыре варианта:
|
| 100 |
+
|
| 101 |
+
1. **Дообучить сам Qwen3-0.6B**, чтобы он имитировал выход 4B. Дороже (трогаем 596M),
|
| 102 |
+
ломает модель для любых других применений и требует учителя «в цикле» при каждом
|
| 103 |
+
использовании — а нам нужен маленький замороженный артефакт.
|
| 104 |
+
2. **Линейная проекция** `6144 → 7680` (47M). Не проверяли отдельно, но вся лестница
|
| 105 |
+
ёмкости показывает, что одной матрицы тут мало: области значений у энкодеров разной
|
| 106 |
+
глубины и ширины, соответствие нелинейное.
|
| 107 |
+
3. **MLP** — выбрали его. Дешевле всех в обучении, учится по тексту без картинок, работает
|
| 108 |
+
побатчно и параллельно по токенам.
|
| 109 |
+
4. **Transformer/Perceiver** — оставили как ступень развития (см. п. 4).
|
| 110 |
+
|
| 111 |
+
Форма MLP — `6144 → 8192 → 8192 → 7680`, три линейных слоя с двумя GELU(tanh):
|
| 112 |
+
лестница ёмкости 44M → 104M → 180M даёт монотонный выигрыш (32.0% → 29.9% → 25.4%).
|
| 113 |
+
В этой лестнице менялись две вещи сразу — ширина и число входных слоёв (3072 против 6144),
|
| 114 |
+
поэтому вклад именно ширины мы не выделяли: выигрыш дают и ёмкость, и более широкий вход.
|
| 115 |
+
Ясно только, что ��альше 180M упирается уже не в параметры, а в данные.
|
| 116 |
+
|
| 117 |
+
### Почему одной поточечной MLP мало
|
| 118 |
+
|
| 119 |
+
Поточечная MLP обрабатывает каждый токен **независимо** — контекста соседей у неё нет.
|
| 120 |
+
А смысл в тексте распределён по токенам: `a bulldog` токенизатор режет как
|
| 121 |
+
`['a', 'Ġbulld', 'og']`, `a rottweiler` — как `['ro', 'tt', 'we', 'iler']`,
|
| 122 |
+
`striped bowtie` — как `['strip', 'ed', 'Ġbow', 'tie']`. Собрать «бульдога» из `Ġbulld`
|
| 123 |
+
и `og` поточечное отображение не может: у него нет способа посмотреть на соседа.
|
| 124 |
+
|
| 125 |
+
Это видно и в цифрах: если токен совпадает с целым словом, перенос почти идеальный
|
| 126 |
+
(`a red fox` — 23.8%, `an orange cat` — 24.0%), а на обрубках ошибка резко выше
|
| 127 |
+
(`a bulldog` — 26.6%, `a rottweiler` — 33.1%).
|
| 128 |
+
|
| 129 |
+
Было две попытки добавить контекст.
|
| 130 |
+
|
| 131 |
+
**Попытка 1 — окно соседей (v10).** Первый слой получает не один токен, а срез
|
| 132 |
+
`[x_{i-1}, x_i, x_{i+1}]` (281.1M параметров). Соседи действительно обучились:
|
| 133 |
+
нормы блоков `‖W_лев‖ = 15.9`, `‖W_прав‖ = 15.0` против `‖W_центр‖ = 124.0` — то есть модель
|
| 134 |
+
честно использует контекст. Но выигрыш ровный ~1 п.п. **на всех** промптах, включая цельные
|
| 135 |
+
слова, а худший обрубок `og` не сдвинулся вообще (46.1% и до, и после). Вывод: локальное окно
|
| 136 |
+
не решает задачу — «бульдог» собирается не из соседних токенов, а требует внимания
|
| 137 |
+
ко всему предложению.
|
| 138 |
+
|
| 139 |
+
**Попытка 2 — residual-ветка внимания (v11, в v14 она же).** Поверх MLP добавлен
|
| 140 |
+
самостоятельный модуль: `Attention(x) = out(блоки(norm(x)))`, где `out` инициализирован
|
| 141 |
+
**нулями**. Это принципиально: на старте обучения адаптер **побитово равен** старой поточечной
|
| 142 |
+
MLP, то есть новая архитектура не может испортить уже выученное, а обучение начинается
|
| 143 |
+
как точное дообучение. Дальше ветка сама набирает вес.
|
| 144 |
+
|
| 145 |
+
Параметры выбраны скромно: 2 блока, `d=1024`, 8 голов, 39.6M — всего 18% от общего размера.
|
| 146 |
+
Одного блока, скорее всего, хватило бы, но два дали лучший результат из измеренного, а цена
|
| 147 |
+
в 40M при бюджете 220M незначительна. Аттеншн без причинности: текст не автогенеративный,
|
| 148 |
+
ограничивать токен предыдущими незачем.
|
| 149 |
+
|
| 150 |
+
### Лосс и метрика
|
| 151 |
+
|
| 152 |
+
**Лосс:** MSE по токенам с пер-токенным весом
|
| 153 |
+
|
| 154 |
+
```
|
| 155 |
+
вес_i = 1 / max(‖y_i‖², 300²), паддинг: 0.1
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
Без весов лосс на 97% состоит из ошибки **пяти служебных токенов шаблона** (attention-sink):
|
| 159 |
+
их норма ~6000 против ~180 у смысловых. Модель честно оптимизировала бы эти пять токенов,
|
| 160 |
+
а промпт не учила бы вовсе. Поэтому:
|
| 161 |
+
|
| 162 |
+
* `--norm-floor 300` — порог, выше которого токен считается «крупным» и его вклад
|
| 163 |
+
нормируется по норме;
|
| 164 |
+
* `--drop-first 5` — первые пять токенов шаблона не идут ни в адаптер, ни в цель (срезка
|
| 165 |
+
делается **после** адаптера, иначе позиционные ветки видели бы 251 токен на обучении
|
| 166 |
+
и 256 на инференсе);
|
| 167 |
+
* `--pad-weight 0.1` — паддинг-позиции участвуют, но с малым весом.
|
| 168 |
+
|
| 169 |
+
**Метрика:** относительная ошибка по токену `‖pred − y‖ / ‖y‖`, отдельно по трём группам:
|
| 170 |
+
|
| 171 |
+
| группа | что это | токенов | средняя ‖y‖ | отн. ошибка |
|
| 172 |
+
|---|---|---|---|---|
|
| 173 |
+
| обычные | смысловые токены промпта (‖y‖ < 300) | 1499 (16 капшенов) | 180.7 | **21.3%** |
|
| 174 |
+
| крупные | attention-sink (‖y‖ ≥ 300), после `drop-first` их почти нет | 0–1 | ~6000 | — |
|
| 175 |
+
| паддинг | позиции после конца промпта — в последовательности их ~63% | 2517 | 143.5 | **60.0%** |
|
| 176 |
+
|
| 177 |
+
Паддинг не падает и не должен: содержания промпта в нём нет, вес в лоссе 0.1, а цель там —
|
| 178 |
+
обобщённый «режим ассистента» учителя с почти постоянной нормой (разброс по позициям ±18 при
|
| 179 |
+
средней 143.5). Смотреть надо только на «обычные»; ноль на паддинге означал бы зубрёжку
|
| 180 |
+
константы.
|
| 181 |
+
|
| 182 |
+
Отдельно про грабли: **element-wise MSE и R² здесь нельзя использовать**. В референсной
|
| 183 |
+
работе приведён `R² = 0.9995` — но он измерен без маски, и эти 0.9995 на 99.6% состоят из
|
| 184 |
+
подгонки тех самых пяти синк-токенов. Мы через это прошли и получили ровно такую же
|
| 185 |
+
«отличную» цифру при реальной относительной ошибке 36–38%.
|
| 186 |
+
|
| 187 |
+
---
|
| 188 |
+
|
| 189 |
+
## 3. Как обучали
|
| 190 |
+
|
| 191 |
+
Команда финального прогона:
|
| 192 |
+
|
| 193 |
+
```bash
|
| 194 |
+
python3 train_adapter.py --ds-path datasets \
|
| 195 |
+
--max-per-group 3000 --ds-skip testg_320_640_temp --seed 42 \
|
| 196 |
+
--drop-first 5 --attention 2 --attn-dim 1024 --attn-heads 8 \
|
| 197 |
+
--empty-ratio 0.05 --epochs 8 --batch-size 32 --log-every 100 --eval-every 1 \
|
| 198 |
+
--student-layers 2,9,14,18,23,27 --hidden 8192 --proj-layers 3 --norm ln-per-layer \
|
| 199 |
+
--lr 1e-4 --attn-lr 2e-4 --lr-schedule cos --epoch-checkpoints \
|
| 200 |
+
--resume adapter_v12_full.pt --adapter-out adapter_v14_bal.pt
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
**Без кэша энкодеров.** Оба энкодера живут онлайн в VRAM (учитель 7.5 ГБ в bf16), кэша
|
| 204 |
+
эмбеддингов нет. Это дороже по памяти (пик 18.5 ГиБ), зато любой датасет и любой текст
|
| 205 |
+
тренируются сразу, а не требуют предварительного прогона. Один шаг — 0.68 с (batch 32,
|
| 206 |
+
длина 256), эпоха — 568 шагов, ~7 минут.
|
| 207 |
+
|
| 208 |
+
**Данные и их состав.** Первый прогон был на 13 163 капшенах, потом 64 450, 95 992, 16 157 —
|
| 209 |
+
и каждый раз цифра улучшалась на десятые доли. Главное открытие оказалось не в объёме, а
|
| 210 |
+
в **составе**:
|
| 211 |
+
|
| 212 |
+
* обучение на полном пуле (323 173 капшена, 86% которого — один прозаический датасет)
|
| 213 |
+
даёт модель, которая **уезжает в прозу**: короткие промпты ухудшаются на 2–3 п.п.,
|
| 214 |
+
и на картинках это видно (бульдог становится всё менее похож);
|
| 215 |
+
* поэтому v14 учили на **сбалансированном пуле**: по 3000 капшенов на группу источников
|
| 216 |
+
(`--max-per-group` группирует по верхней папке датасета), включая danbooru-теги
|
| 217 |
+
с вычищенной служебной разметкой `*tags*`;
|
| 218 |
+
* v14 (19 157 капшенов) обошёл v12 (323 173) **по всем** колонкам. Состав важнее размера.
|
| 219 |
+
|
| 220 |
+
**Пустые промпты обязательны.** `--empty-ratio 0.05` — 5% обучающих примеров с пустым
|
| 221 |
+
промптом. Это нужно не для качества условия, а для CFG: у klein-base ветка uncond считается
|
| 222 |
+
по пустому промпту через тот же адаптер, и если её не учить, при `guidance_scale=4.0`
|
| 223 |
+
картинка портится. Наглядно: v11 (без пустых) — ошибка uncond **20.2%**, v12/v14 (с пустыми) —
|
| 224 |
+
**3.0% / 2.3%**. Разница в 8 раз — и именно она решает, будет ли CFG чистым.
|
| 225 |
+
|
| 226 |
+
**Расписание.** Обучение с нуля — 3e-4 с косинусом, дообучение — 1e-4 (MLP) и 2e-4 (ветка
|
| 227 |
+
внимания) с косинусом до нуля. Ветка внимания берёт свой LR: у неё нулевая инициализация,
|
| 228 |
+
и ей нужен чуть больший шаг, чтобы набрать вес за разумное время.
|
| 229 |
+
|
| 230 |
+
**Резюм и падения.** Прогон v14 один раз умер посреди эпохи (оборванный процесс, без
|
| 231 |
+
трейсбека). Чекпоинт `.last.pt` пишется каждую эпоху и содержит веса **и** оптимизатор **и**
|
| 232 |
+
номер эпохи, поэтому продолжение — это одна строка `--resume ...last.pt` при `--epochs`
|
| 233 |
+
больше пройденного. Срезы по эпохам (`--epoch-checkpoints`) пишутся отдельно, чтобы выбирать
|
| 234 |
+
не «последнюю», а лучшую эпоху.
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## 4. Что получилось
|
| 239 |
+
|
| 240 |
+
### Версии (одни и те же данные: val 627 капшенов + шесть коротких промптов)
|
| 241 |
+
|
| 242 |
+
| чекпоинт | bulldog | rottweiler | striped | red fox | orange cat | теги | пустой | негатив | val 627 |
|
| 243 |
+
|---|---|---|---|---|---|---|---|---|---|
|
| 244 |
+
| v8 — поточечная 180M | 29.18 | 35.80 | 29.37 | 26.92 | 26.69 | 23.09 | 4.64 | 37.40 | 24.36 |
|
| 245 |
+
| v11 — +внимание, без пустых | 27.17 | 34.18 | 27.06 | 24.60 | 24.11 | 22.40 | 20.23 | 36.85 | 23.73 |
|
| 246 |
+
| v12 — 2 эпохи, полный пул 323k | 29.52 | 35.82 | 30.02 | 26.99 | 27.29 | 23.70 | 2.99 | 38.58 | 23.83 |
|
| 247 |
+
| **v14 — 8 эпох, баланс + теги** | **26.56** | **33.12** | **25.17** | **23.77** | **23.95** | **20.19** | **2.27** | **35.59** | **21.66** |
|
| 248 |
+
|
| 249 |
+
Исторические прогоны (v5–v7) учились на других val-наборах и в эту таблицу не включены:
|
| 250 |
+
их цифры 25.43 / 24.96 / 24.69% несопоставимы с «627» строками.
|
| 251 |
+
|
| 252 |
+
### Лестница эпох v14 (тот же прогон, те же данные)
|
| 253 |
+
|
| 254 |
+
| эпоха | bulldog | striped | red fox | orange | теги | пустой | val 627 |
|
| 255 |
+
|---|---|---|---|---|---|---|---|
|
| 256 |
+
| ep0 | 27.79 | 26.92 | 25.12 | 25.34 | 21.92 | 3.50 | 22.87 |
|
| 257 |
+
| ep1 | 27.20 | 26.17 | 24.53 | 24.70 | 20.94 | 3.23 | 22.55 |
|
| 258 |
+
| ep2 | 27.04 | 25.81 | 24.32 | 24.62 | 21.02 | 2.93 | 22.21 |
|
| 259 |
+
| ep3 | 26.77 | 25.41 | 24.11 | 24.25 | 20.46 | 2.70 | 22.01 |
|
| 260 |
+
| ep4 | 26.58 | 25.25 | 23.80 | 24.03 | 20.42 | 2.57 | 21.85 |
|
| 261 |
+
| ep5 | 26.61 | 25.30 | 23.87 | 24.12 | 20.32 | 2.40 | 21.73 |
|
| 262 |
+
| ep6 | 26.56 | 25.19 | 23.81 | 24.00 | 20.21 | 2.29 | 21.67 |
|
| 263 |
+
| **ep7** | 26.56 | **25.17** | **23.77** | **23.95** | **20.19** | **2.27** | **21.66** |
|
| 264 |
+
|
| 265 |
+
Кривая монотонна, но к концу насыщается: ep5 → ep7 добавляют по 0.03–0.06 п.п. Восемь эпох
|
| 266 |
+
на сбалансированном пуле — примерно там, где дальнейшее обучение перестаёт платить за себя.
|
| 267 |
+
Побочный приятный эффект: uncond падает с 3.50% до 2.27% — CFG становится чище от эпохи
|
| 268 |
+
к эпохе, ничего специально для этого делать не нужно.
|
| 269 |
+
|
| 270 |
+
### Что переносится, а что нет (по картинкам)
|
| 271 |
+
|
| 272 |
+
Панели «родной Qwen3-4B | Qwen3-0.6B + адаптер», дистиллированная klein, 4 шага, один сид
|
| 273 |
+
(`media/` в репозитории):
|
| 274 |
+
|
| 275 |
+
* **переносится**: сцена, свет, композиция, поза, одежда, стиль, рендер текста — надпись
|
| 276 |
+
«Qwen3-0.6b» на табличке верна; аниме и danbooru-теги (кошачьи уши, бант, хвост, клыки,
|
| 277 |
+
поза) работают; прозаический портрет женщины совпадает с родным почти неотличимо;
|
| 278 |
+
* **не переносится**: **идентичность объекта**. «Бульдог» выходит терьером, «ротвейлер» —
|
| 279 |
+
тоже не ротвейлером. Это лучшая из наших цифр по бульдогу (26.56 против 29.18 у поточечной),
|
| 280 |
+
и всё равно порода не та.
|
| 281 |
+
|
| 282 |
+
И отдельная методическая вещь, которую стоит держать в голове: **цифра не предсказывает
|
| 283 |
+
картинку**. v12 при лучшем val, чем v11, был хуже по коротким промптам и заметно хуже уводил
|
| 284 |
+
модель в прозу. Поэтому решение о публикации принималось по семплам, а замеры использовались
|
| 285 |
+
как навигация, а не как критерий приёмки.
|
| 286 |
+
|
| 287 |
+
---
|
| 288 |
+
|
| 289 |
+
## 5. Что не сработало (и почему это полезно знать)
|
| 290 |
+
|
| 291 |
+
| попытка | результат |
|
| 292 |
+
|---|---|
|
| 293 |
+
| окно соседей `k=1` вместо внимания (v10) | соседи обучились (‖W‖ 15.9/15.0 против 124.0 у центра), выигрыш ровный ~1 п.п., худший обрубок не сдвинулся |
|
| 294 |
+
| негативные промпты в обучении (v9, `--neg-ratio`) | ошибка негативов упала 37.4% → 15.7%, **но** появились синева и пятна на картинках при одном и том же сиде; откатили |
|
| 295 |
+
| обучение на полном пуле 323k (v12) | val улучшился, короткие промпты ухудшились — модель уехала в прозу |
|
| 296 |
+
| валидация по проз��ическому val | скрыла провал на коротких промптах (проза доминировала в цифре) |
|
| 297 |
+
| element-wise MSE / R² как метрика | измеряет пять синк-токенов: «R² 0.9995» при реальной ошибке 36–38% |
|
| 298 |
+
| первые версии без `--drop-first` | 97% лосса — служебные токены шаблона |
|
| 299 |
+
| шесть слоёв **без** `ln-per-layer` | ранний слой даёт 0.05% входа, т.е. шести слоёв как будто нет |
|
| 300 |
+
|
| 301 |
+
---
|
| 302 |
+
|
| 303 |
+
## 6. Инженерные грабли
|
| 304 |
+
|
| 305 |
+
* **fp8 для энкодера учителя.** Официальный fp8-файл klein diffusers не читает
|
| 306 |
+
(`chunk expects at least a 1-dimensional tensor` на scale-тензорах). Работает
|
| 307 |
+
`QuantoConfig(weights="float8")` через optimum-quanto (устаревший, но живой);
|
| 308 |
+
torchao-вариант с текущими torch молча не срабатывает.
|
| 309 |
+
* **`is_distilled` в klein.** Дистиллированная klein молча отключает CFG
|
| 310 |
+
(`do_classifier_free_guidance = guidance_scale > 1 and not is_distilled`), а в пайплайне
|
| 311 |
+
нет `negative_prompt` — только `negative_prompt_embeds`. Для экспериментов с CFG нужен
|
| 312 |
+
неизбелённый близнец `FLUX.2-klein-base-4B`; при загрузке из локальной папки
|
| 313 |
+
`model_index.json` надо править на `is_distilled: false`, иначе CFG отключится незаметно.
|
| 314 |
+
* **Два флага `--len`.** Длина условия 256, но первые пять токенов шаблона — синк;
|
| 315 |
+
срезка обязана быть в одном и том же месте на обучении и инференсе, иначе позиционные
|
| 316 |
+
ветки видят разное число токенов.
|
| 317 |
+
* **Скорость.** Дистиллированная klein fp8, 4 шага — 1–2 с на картинку 768×1280;
|
| 318 |
+
base fp8, 50 шагов с CFG 4 — 18–30 с; форвард энкодера учителя — ~0.3 с.
|
| 319 |
+
|
| 320 |
+
---
|
| 321 |
+
|
| 322 |
+
## 7. Открытые вопросы
|
| 323 |
+
|
| 324 |
+
1. **Идентичность объекта** — главный провал. Данные и эпохи его не лечат (v6: +51k капшенов
|
| 325 |
+
и +1 эпоха дали ту же цифру и ту же картинку). Следующие нерадикальные ступени:
|
| 326 |
+
дилатация окна соседей, второй уровень смешивания, token-mixing MLP (ранг 64, +0.2M).
|
| 327 |
+
2. **Нет критерия приёмки по существу.** Правильный тест — сравнить поле скоростей klein-DiT
|
| 328 |
+
с родным условием и через адаптер (одни латенты, один t) и решать по этой цифре,
|
| 329 |
+
а не по проценту ошибки условия.
|
| 330 |
+
3. **Нужен ли адаптер вообще.** Если в конкретном пайплайне можно позволить себе Qwen3-4B
|
| 331 |
+
(+7.5 ГБ VRAM, ~0.3 с/шаг), адаптер не нужен: кормите учителя родным энкодером,
|
| 332 |
+
а наш 0.6B оставьте студенту. Адаптер — это размен «качество против VRAM и скорости».
|
| 333 |
+
4. **Сравнение 6 слоёв против 12 и 28** — не измерено, мы остановились на рабочем наборе.
|
| 334 |
+
|
| 335 |
+
---
|
| 336 |
+
|
| 337 |
+
## 8. Как повторить
|
| 338 |
+
|
| 339 |
+
```bash
|
| 340 |
+
# 1. Обучение с нуля (сбалансированный пул + теги, ветка внимания с нуля)
|
| 341 |
+
python3 src/train_adapter.py --ds-path datasets --max-per-group 3000 --seed 42 \
|
| 342 |
+
--drop-first 5 --attention 2 --attn-dim 1024 --attn-heads 8 --empty-ratio 0.05 \
|
| 343 |
+
--epochs 8 --batch-size 32 --student-layers 2,9,14,18,23,27 \
|
| 344 |
+
--hidden 8192 --proj-layers 3 --norm ln-per-layer --lr 3e-4 --attn-lr 2e-4 \
|
| 345 |
+
--lr-schedule cos --epoch-checkpoints --adapter-out adapter_v14_bal.pt
|
| 346 |
+
|
| 347 |
+
# 2. Экспорт в safetensors (схема пишется в метаданные файла, self-check внутри)
|
| 348 |
+
python3 src/export_safetensors.py --in adapter_v14_bal.pt --attn-heads 8 \
|
| 349 |
+
--student-layers 2,9,14,18,23,27 --drop-first 5 --len 256
|
| 350 |
+
|
| 351 |
+
# 3. Использование: подменяем у пайплайна только расчёт условия
|
| 352 |
+
python3 example.py "A red fox walks through a snowy forest at dusk." fox.png
|
| 353 |
+
```
|
| 354 |
+
|
| 355 |
+
Требования: GPU с ~24 ГБ (оба энкодера онлайн + адаптер + активации, пик обучения 18.5 ГиБ),
|
| 356 |
+
diffusers с поддержкой `Flux2KleinPipeline`, transformers с Qwen3, и для fp8-учителя —
|
| 357 |
+
optimum-quanto.
|
src/export_safetensors.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Экспорт обученного адаптера в safetensors (+ метаданные схемы внутри файла).
|
| 3 |
+
|
| 4 |
+
python3 export_safetensors.py --in adapter_v6_180M.pt --student-layers 2,9,14,18,23,27 \
|
| 5 |
+
--drop-first 5 --len 256 --dtype bf16
|
| 6 |
+
|
| 7 |
+
Метаданные кладутся в сам файл, поэтому примеру/тесту не нужны флаги --hidden,
|
| 8 |
+
--proj-layers, --norm: они выводятся из весов (adapter_lib.infer_arch).
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
ap = argparse.ArgumentParser(description="adapter .pt -> .safetensors")
|
| 18 |
+
ap.add_argument("--in", dest="src", required=True)
|
| 19 |
+
ap.add_argument("--out", default=None, help="по умолчанию: то же имя с .safetensors")
|
| 20 |
+
ap.add_argument("--dtype", default="fp32", choices=["fp32", "bf16"],
|
| 21 |
+
help="bf16: файл вдвое меньше; загрузчик поднимает веса в fp32 при инференсе")
|
| 22 |
+
ap.add_argument("--student-layers", default="2,9,14,18,23,27")
|
| 23 |
+
ap.add_argument("--teacher-layers", default="9,18,27")
|
| 24 |
+
ap.add_argument("--drop-first", type=int, default=5)
|
| 25 |
+
ap.add_argument("--len", type=int, default=256)
|
| 26 |
+
ap.add_argument("--attn-heads", type=int, default=8, help="число голов ветки внимания (из весов не выводится)")
|
| 27 |
+
args = ap.parse_args()
|
| 28 |
+
|
| 29 |
+
from adapter_lib import load_adapter, save_safetensors
|
| 30 |
+
|
| 31 |
+
out = args.out or os.path.splitext(args.src)[0] + ".safetensors"
|
| 32 |
+
dtype = torch.float32 if args.dtype == "fp32" else torch.bfloat16
|
| 33 |
+
adapter, meta = load_adapter(args.src, device="cpu", dtype=dtype,
|
| 34 |
+
n_slices=len(args.student_layers.split(",")))
|
| 35 |
+
md = {"student_layers": args.student_layers, "teacher_layers": args.teacher_layers,
|
| 36 |
+
"drop_first": args.drop_first, "len": args.len, "format": "sdxs-micro text adapter",
|
| 37 |
+
"source": os.path.basename(args.src),
|
| 38 |
+
"hidden": meta["hidden"], "proj_layers": meta["proj_layers"], "norm": meta["norm"],
|
| 39 |
+
"attention": meta["attention"], "attn_heads": args.attn_heads}
|
| 40 |
+
if meta["attention"]:
|
| 41 |
+
md["attn_dim"] = meta["attn_dim"]
|
| 42 |
+
save_safetensors(adapter, out, md)
|
| 43 |
+
n = sum(p.numel() for p in adapter.parameters())
|
| 44 |
+
extra = (f", ветка внимания {meta['attention']}x{meta['attn_dim']}/{args.attn_heads} голов"
|
| 45 |
+
if meta["attention"] else "")
|
| 46 |
+
print(f"{out}: {n / 1e6:.1f}M параметров, {os.path.getsize(out) / 2**20:.0f} МБ, "
|
| 47 |
+
f"{args.dtype}, norm={meta['norm']}, {meta['in_dim']} -> {meta['hidden']} -> "
|
| 48 |
+
f"{meta['out_dim']}, слои ТЕ {args.student_layers}, drop_first {args.drop_first}{extra}")
|
| 49 |
+
|
| 50 |
+
# самопроверка: перечитали файл — веса те же (иначе смысла в экспорте нет)
|
| 51 |
+
back, meta2 = load_adapter(out, device="cpu", dtype=dtype)
|
| 52 |
+
d = max((a - b).abs().max().item() for a, b in
|
| 53 |
+
zip(back.state_dict().values(), adapter.state_dict().values()))
|
| 54 |
+
print(f"self-check: max|diff| весов после перечитывания {d:.1e}, "
|
| 55 |
+
f"n_slices из метаданных {meta2['n_slices']}")
|
| 56 |
+
assert d < 1e-6
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
main()
|