Когда я решил провести простой ablation на 75 картинках, я ожидал увидеть постепенную деградацию метрик. Красивый график, пара строчек в логе — и можно идти пить кофе. Вместо этого я получил data leak, который перечеркнул неделю обучения, и коллапс модели при scale 2.0, который выглядел так, будто LoRA внезапно решила забыть всё, что умела.
Добро пожаловать в реальный мир FLUX-LoRA. Здесь каждый гиперпараметр — минное поле, а стандартные практики Stable Diffusion не работают. Сегодня я расскажу, как 75 картинок обрушили мой пайплайн и почему single-pass стал единственным спасением.
Дисклеймер: всё описанное — реальный кейс апреля 2026 года. Версия FLUX.2 в Diffusers 0.38.0, бэкенд PyTorch 2.6. Если вы используете FP8 — сначала прочитайте статью про underflow — там сценарий ещё жёстче.
Пайплайн, который казался идеальным
Я собирал LoRA для генерации реалистичных портретов людей в определённом стиле. Dataset — 5000 тщательно отобранных изображений, размеченных по 20 концептам. Использовал стандартный multi-pass: 3 эпохи, каждая картинка видится 3 раза с разными аугментациями. Scale LoRA — 1.0 (по умолчанию), learning rate — 1e-4, оптимизатор AdamW.
Пайплайн на базе FLUX.2 в Diffusers выглядел так:
from diffusers import FluxPipeline, FluxTransformer2DModel
from peft import LoraConfig, get_peft_model
transformer = FluxTransformer2DModel.from_pretrained("black-forest-labs/FLUX.2-dev")
lora_config = LoraConfig(r=16, lora_alpha=32, target_modules=["to_q", "to_k", "to_v", "to_out.0"])
transformer = get_peft_model(transformer, lora_config)
pipe = FluxPipeline.from_pretrained("black-forest-labs/FLUX.2-dev", transformer=transformer)
pipe.to("cuda")На первый взгляд — ничего особенного. LoRA использует тот же формат, что и для Stable Diffusion. Но дьявол, как всегда, в деталях.
75 картинок, которые всё сломали
Для быстрой проверки переобучения я решил сделать ablation: взять случайную подвыборку из 75 изображений (из разных концептов) и обучить LoRA только на них. Идея простая — если модель запоминает картинки, то на малом датасете качество должно быть идеальным, а при увеличении выборки — ухудшаться. Я ожидал плавного роста метрик.
Реальность оказалась другой. После 5 эпох на 75 картинках модель выдавала на валидации точность 98% — но при этом генерировала почти точные копии тренировочных изображений. Я проверил: среднее косинусное расстояние между сгенерированными картинками и ближайшими трейновыми образцами упало до 0.15. Для сравнения, на полном датасете оно было 0.42.
1Как я поймал leak
Написал скрипт, который для каждого сгенерированного изображения ищет ближайшего соседа в тренировочном датасете по SSIM и LPIPS. Если доля «дубликатов» (SSIM > 0.9) превышает 5% — это красный флаг.
from skimage.metrics import structural_similarity as ssim
import numpy as np
def detect_leak(generated, train_dataset, threshold=0.9):
dup_count = 0
for gen in generated:
for train_img in train_dataset:
if ssim(gen, train_img) > threshold:
dup_count += 1
break
return dup_count / len(generated)
leak_rate = detect_leak(validation_images, train_dataset)
print(f"Leak rate: {leak_rate:.2%}")На 75 картинках leak rate составил 32%. На полном датасете — 0.8%. Вывод: при малом количестве данных LoRA просто запоминает всю выборку, а не обобщает. Проблема усугубляется тем, что multi-pass (каждая картинка видится 3 раза) увеличивает шанс запоминания.
Scale 2.0: когда LoRA сходит с ума
После обнаружения leak я решил экспериментировать с масштабом LoRA. В Diffusers есть параметр lora_scale — множитель, который умножает дельту весов при инференсе. Многие (включая меня до недавнего времени) думают: «Увеличу scale — получу более сильный эффект». На практике при scale 2.0 я получил полный коллапс.
Модель начала выдавать шум или одинаковые серые пятна. Почему? Потому что FLUX использует схему слияния LoRA в диффузионном процессе пошагово, и при удвоении scale градиенты на некоторых шагах уходят за пределы плавающей точки (особенно на FP16). Подробнее об этом — в статье про FP8 и underflow, но суть та же: численная нестабильность.
❗ Важный нюанс: в Diffusers 0.38.0 lora_scale применяется после накопления дельт в multi-pass пайплайне. Если вы собрали несколько LoRA или запустили multi-pass с разными scale — результат может оказаться нелинейным. Я наступил на эти грабли: в пайплайне был второй проход с scale 1.2, а финальный — 2.0. Итог: коллапс на 10-м шаге.
2Вскрытие коллапса
Я залогировал нормы LoRA-весов на каждом шаге инференса. При scale 1.0 нормы оставались в пределах (0, 1). При scale 2.0 после 3-го шага одна из матриц to_q дала норму 7.3, а на 5-м — NaN. Это типичный вынос экспоненты из-за слишком большого масштаба на чувствительных слоях.
import torch
def check_lora_norms(pipe, step):
lora_layers = [m for m in pipe.transformer.modules() if hasattr(m, 'lora_A')]
norms = {}
for layer in lora_layers:
W = layer.lora_A.data @ layer.lora_B.data
norms[layer.name] = torch.norm(W).item()
return norms
# Использование:
pipe.unload_lora_weights() # очищаем исходные
pipe.load_lora_weights("path/to/lora", scale=2.0)
norms = check_lora_norms(pipe, step=5)
print(norms)Совет: никогда не ставьте scale > 1.0 без предварительного ablation на 10-20 случайных промптах. Я бы даже рекомендовал зашить проверку в CI.
Single-pass как спаситель
Проблемы leak и коллапса заставили меня пересмотреть пайплайн. Решение пришло из неожиданной области — из практик fine-tuning больших языковых моделей, где multi-pass давно признан вредным (см. лоботомические слои).
Я перешёл на single-pass: каждая картинка показывается модели ровно один раз. Learning rate пришлось поднять до 3e-4, batch size увеличить до 16 (чтобы компенсировать меньшее количество шагов). Scale оставил 0.8 — намеренно меньше, чем единица, чтобы избежать перенасыщения.
| Параметр | Multi-pass (old) | Single-pass (new) |
|---|---|---|
| Epochs | 3 | 1 |
| Steps per image | 3 | 1 |
| Leak rate (75 images) | 32% | 1.2% |
| Collapse at scale 2.0 | Yes | No (still NaN after 1.5) |
| Quality (FID) | 18.4 | 12.7 |
Результат: FID улучшился на 31%, leak практически исчез, а коллапс при scale 2.0 больше не воспроизводится (хотя при scale > 1.5 всё ещё возможен — но это уже разумный предел).
Чтобы быстро протестировать такие конфигурации на разных GPU, я использую облачные инстансы RunPod — они позволяют запускать ablation параллельно на нескольких картах за копейки.
Как не наступать на эти грабли
- Всегда делайте ablation на маленькой выборке (20-100 изображений), чтобы выявить leak и подобрать learning rate.
- Не верьте дефолтам — scale 1.0 может быть слишком большим для некоторых LoRA. Тестируйте 0.5, 0.7, 0.9.
- Single-pass вместо multi-pass — меньше шансов на запоминание, выше обобщающая способность.
- Мониторьте нормы LoRA-весов во время инференса — если видите числа > 5, это повод снизить scale.
- Используйте FP32 для обучения (хотя бы в ablation), чтобы исключить численные ошибки. FP16/FP8 — только для продакшена после верификации.
Кстати, проблема переобучения на малых датасетах актуальна и для коммерческих LoRA — про это я писал в материале про дипфейковый базар на Civitai. Там люди за 10 минут создают LoRA на одного человека, не задумываясь о leak, и потом удивляются, что модель рисует не «человека в стиле», а конкретные фотки.
В 2026 году обучение LoRA стало доступнее, но количество «волшебников», которые тыкают кнопки в автоматических пайплайнах, только растёт. Умение читать ablation — вот что отличает инженера от пользователя Civitai.
Если вы используете llama.cpp для тяжелых инференсов — советую проверить его версию: недавно была критическая дыра безопасности.
Не доверяйте scale 2.0 — проверяйте ablation на 75 картинках.