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
File size: 30,240 Bytes
c1ca515 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | # Как обучен адаптер Qwen3-0.6B → Qwen3-4B: подробный разбор
Это технический разбор адаптера [`adapter_v14_bal.safetensors`](adapter_v14_bal.safetensors) —
220M-маппинга из пространства текстового энкодера Qwen3-0.6B в пространство текстового
энкодера FLUX.2-klein-4B (Qwen3-4B). Английская карточка модели — в [README.md](README.md),
здесь то же самое, но с цифрами замеров, историей решений и разбором того, что не сработало.
---
## 1. Зачем вообще адаптер
Учитель в нашей дистилляции — FLUX.2-klein-4B. Его DiT обучен на текстовом условии, которое
считает его собственный энкодер: Qwen3-4B, 36 слоёв, hidden 2560, склейка слоёв **9, 18, 27** —
это `joint_attention_dim = 7680`, и ровно эти три слоя берёт сам пайплайн diffusers.
Наш micro-студент имеет дешёвый энкодер — **Qwen3-0.6B** (28 слоёв, hidden 1024), потому что
в micro-модели весь бюджет уходит в DiT, а не в текст.
Возникает развилка при дистилляции:
* кормить учителя **его родным** условием (нужен Qwen3-4B: +7.5 ГБ VRAM и ~0.3 с на шаг);
* либо кормить учителя **нашим** условием, но переведённым в его пространство.
Второй путь и есть адаптер: `y = A(наш_ТЕ(x))`, где `A` учится по тексту, без картинок,
без VAE и без диффузии. Цель — не «похоже на подсказку», а **в точности то, что дал бы родной
энкодер учителя на том же тексте**. Тогда учитель ведёт студента в том режиме, в каком он
работал всегда, а студент дистиллируется от условия, которое сам и умеет считать.
Ключевое упрощение: задача — чистая регрессия «эмбеддинги в эмбеддинги». Токенизаторы
у Qwen3-0.6B и Qwen3-4B **идентичны** (vocab 151669, тот же `chat_template`, тот же
`padding_side=right`), поэтому позиции ученика и учителя совпадают ровно, без выравнивания.
У референсной работы (`recoilme/sdxs`, `qwen_adapter_2b_4b`) токенизаторы разные, и там
позиции частично разъезжаются — у нас этой проблемы нет.
---
## 2. Как он устроен
```
текст → Qwen3-0.6B (заморожен) → hidden states слоёв 2,9,14,18,23,27 → склейка (B, 256, 6144)
→ адаптер → (B, 256, 7680) → DiT klein
y_i = MLP(x_i) + Attention(x)_i
│ │
│ └─ residual-ветка: 2 блока self-attention, d=1024, 8 голов, 39.6M
└─ поточечная MLP: 6144 → 8192 → 8192 → 7680, GELU(tanh), 180.4M
```
Итого 220.0M параметров. Порядок склейки — послойно-майорный, `[слой2 | слой9 | … | слой27]`,
ровно так же, как klein собирает свои три слоя (`stack(dim=1)` → `permute`).
Применяется адаптер **только к условию учителя**. Наш DiT по-прежнему получает свой 4D-контекст
`(B, L, 6, 1024)` и обрабатывает его сам (`TextFusion` + `txtmlp`) — адаптер существует для
взаимодействия с учителем, а не для студента.
### Почему шесть слоёв, а не три
Соблазн взять симметрию: klein использует три слоя из 36 (25%, 50%, 75%) — значит, и нам взять
три. Мы так и начали, и это оказалось хуже по всем замерам:
| вход адаптера | параметров | отн. ошибка |
|---|---|---|
| 3 слоя → 3072, MLP `3072→4096→7680`, полный промпт | 44M | 36.03% |
| то же, но `--drop-first 5` и 5 датасетов × 3000 | 44M | 32.04% |
| 3 слоя → 3072, MLP `3072→6144→6144→7680` | 104M | 29.92% |
| **6 слоёв → 6144, MLP `6144→8192→8192→7680` + `ln-per-layer`** | **180M** | **25.43%** |
Причин у выигрыша шесть-слоёв две, и обе измеримые.
**Первая — нормы слоёв растут как 13 → 557.** Норма выхода слоя Qwen3-0.6B по глубине
отличается в десятки раз. Если склеить слои как есть, вход адаптера почти целиком состоит из
глубоких слоёв, а ранний слой даёт **0.05%** — то есть его как будто нет. Именно поэтому
шесть слоёв идут вместе с `--norm ln-per-layer` (у каждого среза свои статистики, без
обучаемых параметров): после неё все шесть слоёв сопоставимы по масштабу и каждый начинает
работать. Отдельного замера «шесть слоёв без нормировки» у нас нет — но при таком разбросе
норм он выродился бы в «три глубоких слоя», то есть в 104M-строку таблицы выше.
**Вторая — глубина чтения.** Выход слоя 9 у 36-слойной модели — это абстракция, снятая после
девяти блоков. У 28-слойной модели такой же по смыслу абстракции соответствует не слой 7
(та же доля глубины), а другой слой — и «правильная» глубина чтения зависит от токена: для
поверхностных вещей (регистр, отдельный тег, эмодзи) информативнее ранние слои, для смысла —
поздние. Отдав адаптеру **несколько** глубин, мы позволяем ему выбирать чтение самому, вместо
того чтобы угадывать единственный «правильный» слой. Эмпирика это подтверждает: набор
`2,9,14,18,23,27` — не равномерная сетка (9 и 14 стоят ближе друг к другу), он выбирался
по замерам, а не по формуле.
**Дополнительный аргумент:** ровно этот набор — то, что ест наш micro-DiT
(`TextFusion` → `txtmlp`). Условие адаптера и условие студента — это одни и те же фичи,
разница только в цели (пространство 4B против своего). Для дистилляции это удобно: студент
учится на том же представлении текста, что и потом использует сам.
Почему не взять все 28 слоёв: во-первых, вход первого линейного слоя вырос бы до 28 672
(первый слой стал бы 235M параметров сам по себе), во-вторых, соседние слои сильно
коррелированы, в-третьих, студенту нужны именно эти шесть. Это выбор, а не измеренный
оптимум — сравнения «6 против 12 против 28» у нас нет.
### Почему MLP, а не что-то другое
Рассматривались четыре варианта:
1. **Дообучить сам Qwen3-0.6B**, чтобы он имитировал выход 4B. Дороже (трогаем 596M),
ломает модель для любых других применений и требует учителя «в цикле» при каждом
использовании — а нам нужен маленький замороженный артефакт.
2. **Линейная проекция** `6144 → 7680` (47M). Не проверяли отдельно, но вся лестница
ёмкости показывает, что одной матрицы тут мало: области значений у энкодеров разной
глубины и ширины, соответствие нелинейное.
3. **MLP** — выбрали его. Дешевле всех в обучении, учится по тексту без картинок, работает
побатчно и параллельно по токенам.
4. **Transformer/Perceiver** — оставили как ступень развития (см. п. 4).
Форма MLP — `6144 → 8192 → 8192 → 7680`, три линейных слоя с двумя GELU(tanh):
лестница ёмкости 44M → 104M → 180M даёт монотонный выигрыш (32.0% → 29.9% → 25.4%).
В этой лестнице менялись две вещи сразу — ширина и число входных слоёв (3072 против 6144),
поэтому вклад именно ширины мы не выделяли: выигрыш дают и ёмкость, и более широкий вход.
Ясно только, что дальше 180M упирается уже не в параметры, а в данные.
### Почему одной поточечной MLP мало
Поточечная MLP обрабатывает каждый токен **независимо** — контекста соседей у неё нет.
А смысл в тексте распределён по токенам: `a bulldog` токенизатор режет как
`['a', 'Ġbulld', 'og']`, `a rottweiler` — как `['ro', 'tt', 'we', 'iler']`,
`striped bowtie` — как `['strip', 'ed', 'Ġbow', 'tie']`. Собрать «бульдога» из `Ġbulld`
и `og` поточечное отображение не может: у него нет способа посмотреть на соседа.
Это видно и в цифрах: если токен совпадает с целым словом, перенос почти идеальный
(`a red fox` — 23.8%, `an orange cat` — 24.0%), а на обрубках ошибка резко выше
(`a bulldog` — 26.6%, `a rottweiler` — 33.1%).
Было две попытки добавить контекст.
**Попытка 1 — окно соседей (v10).** Первый слой получает не один токен, а срез
`[x_{i-1}, x_i, x_{i+1}]` (281.1M параметров). Соседи действительно обучились:
нормы блоков `‖W_лев‖ = 15.9`, `‖W_прав‖ = 15.0` против `‖W_центр‖ = 124.0` — то есть модель
честно использует контекст. Но выигрыш ровный ~1 п.п. **на всех** промптах, включая цельные
слова, а худший обрубок `og` не сдвинулся вообще (46.1% и до, и после). Вывод: локальное окно
не решает задачу — «бульдог» собирается не из соседних токенов, а требует внимания
ко всему предложению.
**Попытка 2 — residual-ветка внимания (v11, в v14 она же).** Поверх MLP добавлен
самостоятельный модуль: `Attention(x) = out(блоки(norm(x)))`, где `out` инициализирован
**нулями**. Это принципиально: на старте обучения адаптер **побитово равен** старой поточечной
MLP, то есть новая архитектура не может испортить уже выученное, а обучение начинается
как точное дообучение. Дальше ветка сама набирает вес.
Параметры выбраны скромно: 2 блока, `d=1024`, 8 голов, 39.6M — всего 18% от общего размера.
Одного блока, скорее всего, хватило бы, но два дали лучший результат из измеренного, а цена
в 40M при бюджете 220M незначительна. Аттеншн без причинности: текст не автогенеративный,
ограничивать токен предыдущими незачем.
### Лосс и метрика
**Лосс:** MSE по токенам с пер-токенным весом
```
вес_i = 1 / max(‖y_i‖², 300²), паддинг: 0.1
```
Без весов лосс на 97% состоит из ошибки **пяти служебных токенов шаблона** (attention-sink):
их норма ~6000 против ~180 у смысловых. Модель честно оптимизировала бы эти пять токенов,
а промпт не учила бы вовсе. Поэтому:
* `--norm-floor 300` — порог, выше которого токен считается «крупным» и его вклад
нормируется по норме;
* `--drop-first 5` — первые пять токенов шаблона не идут ни в адаптер, ни в цель (срезка
делается **после** адаптера, иначе позиционные ветки видели бы 251 токен на обучении
и 256 на инференсе);
* `--pad-weight 0.1` — паддинг-позиции участвуют, но с малым весом.
**Метрика:** относительная ошибка по токену `‖pred − y‖ / ‖y‖`, отдельно по трём группам:
| группа | что это | токенов | средняя ‖y‖ | отн. ошибка |
|---|---|---|---|---|
| обычные | смысловые токены промпта (‖y‖ < 300) | 1499 (16 капшенов) | 180.7 | **21.3%** |
| крупные | attention-sink (‖y‖ ≥ 300), после `drop-first` их почти нет | 0–1 | ~6000 | — |
| паддинг | позиции после конца промпта — в последовательности их ~63% | 2517 | 143.5 | **60.0%** |
Паддинг не падает и не должен: содержания промпта в нём нет, вес в лоссе 0.1, а цель там —
обобщённый «режим ассистента» учителя с почти постоянной нормой (разброс по позициям ±18 при
средней 143.5). Смотреть надо только на «обычные»; ноль на паддинге означал бы зубрёжку
константы.
Отдельно про грабли: **element-wise MSE и R² здесь нельзя использовать**. В референсной
работе приведён `R² = 0.9995` — но он измерен без маски, и эти 0.9995 на 99.6% состоят из
подгонки тех самых пяти синк-токенов. Мы через это прошли и получили ровно такую же
«отличную» цифру при реальной относительной ошибке 36–38%.
---
## 3. Как обучали
Команда финального прогона:
```bash
python3 train_adapter.py --ds-path datasets \
--max-per-group 3000 --ds-skip testg_320_640_temp --seed 42 \
--drop-first 5 --attention 2 --attn-dim 1024 --attn-heads 8 \
--empty-ratio 0.05 --epochs 8 --batch-size 32 --log-every 100 --eval-every 1 \
--student-layers 2,9,14,18,23,27 --hidden 8192 --proj-layers 3 --norm ln-per-layer \
--lr 1e-4 --attn-lr 2e-4 --lr-schedule cos --epoch-checkpoints \
--resume adapter_v12_full.pt --adapter-out adapter_v14_bal.pt
```
**Без кэша энкодеров.** Оба энкодера живут онлайн в VRAM (учитель 7.5 ГБ в bf16), кэша
эмбеддингов нет. Это дороже по памяти (пик 18.5 ГиБ), зато любой датасет и любой текст
тренируются сразу, а не требуют предварительного прогона. Один шаг — 0.68 с (batch 32,
длина 256), эпоха — 568 шагов, ~7 минут.
**Данные и их состав.** Первый прогон был на 13 163 капшенах, потом 64 450, 95 992, 16 157 —
и каждый раз цифра улучшалась на десятые доли. Главное открытие оказалось не в объёме, а
в **составе**:
* обучение на полном пуле (323 173 капшена, 86% которого — один прозаический датасет)
даёт модель, которая **уезжает в прозу**: короткие промпты ухудшаются на 2–3 п.п.,
и на картинках это видно (бульдог становится всё менее похож);
* поэтому v14 учили на **сбалансированном пуле**: по 3000 капшенов на группу источников
(`--max-per-group` группирует по верхней папке датасета), включая danbooru-теги
с вычищенной служебной разметкой `*tags*`;
* v14 (19 157 капшенов) обошёл v12 (323 173) **по всем** колонкам. Состав важнее размера.
**Пустые промпты обязательны.** `--empty-ratio 0.05` — 5% обучающих примеров с пустым
промптом. Это нужно не для качества условия, а для CFG: у klein-base ветка uncond считается
по пустому промпту через тот же адаптер, и если её не учить, при `guidance_scale=4.0`
картинка портится. Наглядно: v11 (без пустых) — ошибка uncond **20.2%**, v12/v14 (с пустыми) —
**3.0% / 2.3%**. Разница в 8 раз — и именно она решает, будет ли CFG чистым.
**Расписание.** Обучение с нуля — 3e-4 с косинусом, дообучение — 1e-4 (MLP) и 2e-4 (ветка
внимания) с косинусом до нуля. Ветка внимания берёт свой LR: у неё нулевая инициализация,
и ей нужен чуть больший шаг, чтобы набрать вес за разумное время.
**Резюм и падения.** Прогон v14 один раз умер посреди эпохи (оборванный процесс, без
трейсбека). Чекпоинт `.last.pt` пишется каждую эпоху и содержит веса **и** оптимизатор **и**
номер эпохи, поэтому продолжение — это одна строка `--resume ...last.pt` при `--epochs`
больше пройденного. Срезы по эпохам (`--epoch-checkpoints`) пишутся отдельно, чтобы выбирать
не «последнюю», а лучшую эпоху.
---
## 4. Что получилось
### Версии (одни и те же данные: val 627 капшенов + шесть коротких промптов)
| чекпоинт | bulldog | rottweiler | striped | red fox | orange cat | теги | пустой | негатив | val 627 |
|---|---|---|---|---|---|---|---|---|---|
| v8 — поточечная 180M | 29.18 | 35.80 | 29.37 | 26.92 | 26.69 | 23.09 | 4.64 | 37.40 | 24.36 |
| v11 — +внимание, без пустых | 27.17 | 34.18 | 27.06 | 24.60 | 24.11 | 22.40 | 20.23 | 36.85 | 23.73 |
| v12 — 2 эпохи, полный пул 323k | 29.52 | 35.82 | 30.02 | 26.99 | 27.29 | 23.70 | 2.99 | 38.58 | 23.83 |
| **v14 — 8 эпох, баланс + теги** | **26.56** | **33.12** | **25.17** | **23.77** | **23.95** | **20.19** | **2.27** | **35.59** | **21.66** |
Исторические прогоны (v5–v7) учились на других val-наборах и в эту таблицу не включены:
их цифры 25.43 / 24.96 / 24.69% несопоставимы с «627» строками.
### Лестница эпох v14 (тот же прогон, те же данные)
| эпоха | bulldog | striped | red fox | orange | теги | пустой | val 627 |
|---|---|---|---|---|---|---|---|
| ep0 | 27.79 | 26.92 | 25.12 | 25.34 | 21.92 | 3.50 | 22.87 |
| ep1 | 27.20 | 26.17 | 24.53 | 24.70 | 20.94 | 3.23 | 22.55 |
| ep2 | 27.04 | 25.81 | 24.32 | 24.62 | 21.02 | 2.93 | 22.21 |
| ep3 | 26.77 | 25.41 | 24.11 | 24.25 | 20.46 | 2.70 | 22.01 |
| ep4 | 26.58 | 25.25 | 23.80 | 24.03 | 20.42 | 2.57 | 21.85 |
| ep5 | 26.61 | 25.30 | 23.87 | 24.12 | 20.32 | 2.40 | 21.73 |
| ep6 | 26.56 | 25.19 | 23.81 | 24.00 | 20.21 | 2.29 | 21.67 |
| **ep7** | 26.56 | **25.17** | **23.77** | **23.95** | **20.19** | **2.27** | **21.66** |
Кривая монотонна, но к концу насыщается: ep5 → ep7 добавляют по 0.03–0.06 п.п. Восемь эпох
на сбалансированном пуле — примерно там, где дальнейшее обучение перестаёт платить за себя.
Побочный приятный эффект: uncond падает с 3.50% до 2.27% — CFG становится чище от эпохи
к эпохе, ничего специально для этого делать не нужно.
### Что переносится, а что нет (по картинкам)
Панели «родной Qwen3-4B | Qwen3-0.6B + адаптер», дистиллированная klein, 4 шага, один сид
(`media/` в репозитории):
* **переносится**: сцена, свет, композиция, поза, одежда, стиль, рендер текста — надпись
«Qwen3-0.6b» на табличке верна; аниме и danbooru-теги (кошачьи уши, бант, хвост, клыки,
поза) работают; прозаический портрет женщины совпадает с родным почти неотличимо;
* **не переносится**: **идентичность объекта**. «Бульдог» выходит терьером, «ротвейлер» —
тоже не ротвейлером. Это лучшая из наших цифр по бульдогу (26.56 против 29.18 у поточечной),
и всё равно порода не та.
И отдельная методическая вещь, которую стоит держать в голове: **цифра не предсказывает
картинку**. v12 при лучшем val, чем v11, был хуже по коротким промптам и заметно хуже уводил
модель в прозу. Поэтому решение о публикации принималось по семплам, а замеры использовались
как навигация, а не как критерий приёмки.
---
## 5. Что не сработало (и почему это полезно знать)
| попытка | результат |
|---|---|
| окно соседей `k=1` вместо внимания (v10) | соседи обучились (‖W‖ 15.9/15.0 против 124.0 у центра), выигрыш ровный ~1 п.п., худший обрубок не сдвинулся |
| негативные промпты в обучении (v9, `--neg-ratio`) | ошибка негативов упала 37.4% → 15.7%, **но** появились синева и пятна на картинках при одном и том же сиде; откатили |
| обучение на полном пуле 323k (v12) | val улучшился, короткие промпты ухудшились — модель уехала в прозу |
| валидация по прозаическому val | скрыла провал на коротких промптах (проза доминировала в цифре) |
| element-wise MSE / R² как метрика | измеряет пять синк-токенов: «R² 0.9995» при реальной ошибке 36–38% |
| первые версии без `--drop-first` | 97% лосса — служебные токены шаблона |
| шесть слоёв **без** `ln-per-layer` | ранний слой даёт 0.05% входа, т.е. шести слоёв как будто нет |
---
## 6. Инженерные грабли
* **fp8 для энкодера учителя.** Официальный fp8-файл klein diffusers не читает
(`chunk expects at least a 1-dimensional tensor` на scale-тензорах). Работает
`QuantoConfig(weights="float8")` через optimum-quanto (устаревший, но живой);
torchao-вариант с текущими torch молча не срабатывает.
* **`is_distilled` в klein.** Дистиллированная klein молча отключает CFG
(`do_classifier_free_guidance = guidance_scale > 1 and not is_distilled`), а в пайплайне
нет `negative_prompt` — только `negative_prompt_embeds`. Для экспериментов с CFG нужен
неизбелённый близнец `FLUX.2-klein-base-4B`; при загрузке из локальной папки
`model_index.json` надо править на `is_distilled: false`, иначе CFG отключится незаметно.
* **Два флага `--len`.** Длина условия 256, но первые пять токенов шаблона — синк;
срезка обязана быть в одном и том же месте на обучении и инференсе, иначе позиционные
ветки видят разное число токенов.
* **Скорость.** Дистиллированная klein fp8, 4 шага — 1–2 с на картинку 768×1280;
base fp8, 50 шагов с CFG 4 — 18–30 с; форвард энкодера учителя — ~0.3 с.
---
## 7. Открытые вопросы
1. **Идентичность объекта** — главный провал. Данные и эпохи его не лечат (v6: +51k капшенов
и +1 эпоха дали ту же цифру и ту же картинку). Следующие нерадикальные ступени:
дилатация окна соседей, второй уровень смешивания, token-mixing MLP (ранг 64, +0.2M).
2. **Нет критерия приёмки по существу.** Правильный тест — сравнить поле скоростей klein-DiT
с родным условием и через адаптер (одни латенты, один t) и решать по этой цифре,
а не по проценту ошибки условия.
3. **Нужен ли адаптер вообще.** Если в конкретном пайплайне можно позволить себе Qwen3-4B
(+7.5 ГБ VRAM, ~0.3 с/шаг), адаптер не нужен: кормите учителя родным энкодером,
а наш 0.6B оставьте студенту. Адаптер — это размен «качество против VRAM и скорости».
4. **Сравнение 6 слоёв против 12 и 28** — не измерено, мы остановились на рабочем наборе.
---
## 8. Как повторить
```bash
# 1. Обучение с нуля (сбалансированный пул + теги, ветка внимания с нуля)
python3 src/train_adapter.py --ds-path datasets --max-per-group 3000 --seed 42 \
--drop-first 5 --attention 2 --attn-dim 1024 --attn-heads 8 --empty-ratio 0.05 \
--epochs 8 --batch-size 32 --student-layers 2,9,14,18,23,27 \
--hidden 8192 --proj-layers 3 --norm ln-per-layer --lr 3e-4 --attn-lr 2e-4 \
--lr-schedule cos --epoch-checkpoints --adapter-out adapter_v14_bal.pt
# 2. Экспорт в safetensors (схема пишется в метаданные файла, self-check внутри)
python3 src/export_safetensors.py --in adapter_v14_bal.pt --attn-heads 8 \
--student-layers 2,9,14,18,23,27 --drop-first 5 --len 256
# 3. Использование: подменяем у пайплайна только расчёт условия
python3 example.py "A red fox walks through a snowy forest at dusk." fox.png
```
Требования: GPU с ~24 ГБ (оба энкодера онлайн + адаптер + активации, пик обучения 18.5 ГиБ),
diffusers с поддержкой `Flux2KleinPipeline`, transformers с Qwen3, и для fp8-учителя —
optimum-quanto.
|