Бывает, сидишь ночью, доделываешь фичу, тестишь — всё летает. Деплоишь в прод. А через неделю прилетает баг-репорт: «генерация тормозит, картинки мыльные, почему нельзя увеличить качество?». Открываешь код, смотришь — и чувствуешь, как щека начинает дёргаться. В хардкоде сидит num_inference_steps=28.
Я хочу рассказать, как я сам себе прострелил ногу, неправильно прочитав документацию FLUX, и что из этого вышло. Это история про самоограничение, про то, как мы, разработчики, любим «защищать» систему от самих себя, и про то, чем это может обернуться.
Как я выстрелил себе в ногу: предыстория
Всё началось с того, что мы решили добавить в наш сервис генерацию изображений на базе FLUX. Модель тогда была свежая — FLUX.2 в Diffusers только вышла, и мы, как прогрессивная команда, сразу вкатили. Я полез в документацию к пайплайну. Там чёрным по белому написано: num_inference_steps: int, optional, default 28. И где-то в примерах — «обычно 28 шагов даёт хороший баланс скорости и качества».
И тут я, видимо, прочитал это как «максимально допустимое количество шагов — 28, больше нельзя». Сам себе внушил: «FLUX — это диффузионка с каким-то продвинутым scheduler, у неё есть потолок, иначе модель расходится». Бред? Абсолютный. Но в тот момент логика была: «Зачем давать пользователю опцию менять шаги, если от этого только проблемы? Давайте зафиксируем на дефолте».
Так в коде появилась строчка:
# Фиксируем шаги, чтобы не навредить себе
images = pipe(
prompt,
num_inference_steps=28, # МАКСИМУМ! (как я думал)
guidance_scale=3.5
).images
Сразу скажу: код работал. Картинки генерировались, пользователи были довольны. Но я даже не подумал вынести это в конфиг или переменную окружения. «Зачем? Ведь менять никогда не придётся» — классическая ловушка.
Момент истины: баг-репорт и первые подозрения
Через пару недель нашёл тикет от пользователя: «Почему картинки такие мыльные? В Stable Diffusion XL с 50 шагами получается детализированнее». Я полез проверять. Действительно, есть некоторая размытость, особенно на сложных промптах. Но я же помню — 28 шагов, это же стандарт? А потом другой пользователь спросил: «А можно ли ускорить генерацию, уменьшив шаги до 8? Мне нужно быстро прототипировать».
И тут я осознал: я заблокировал возможность менять шаги. Хуже того — я даже не знаю, можно ли их менять вообще. Пошёл перечитывать документацию. И понял, что я идиот. num_inference_steps — это просто параметр, который можно ставить от 1 до разумных 100 и больше. Никакого потолка. Ограничение было только в моей голове.
Важно: В Diffusers для FLUX по умолчанию используется FlowMatchEulerDiscreteScheduler. Он поддерживает переменное количество шагов без проблем. Никаких ограничений на 28 нет. Это я сам себе придумал лимит.
Параллельно вспомнилась история с кривым расчётом key_gdiff в llama.cpp, когда неправильная интерпретация параметра ломала генерацию. Там тоже всё начиналось с «а зачем менять?» — но потом выяснилось, что баг прятался в другом месте. У нас было похоже: мы не давали пользователю гибкости, потому что боялись, что он что-то сломает.
Копание в исходниках FLUX: что на самом деле происходит
Чтобы убедиться, что увеличение шагов безопасно, я залез в код FLUX pipeline. Оказалось, scheduler FlowMatchEulerDiscreteScheduler на каждом шаге просто сэмплирует траекторию потока. Больше шагов — плавнее переход, меньше шумов. Никакого «срыва» модели. Наоборот, для сложных промптов рекомендуется 50-60 шагов, а для быстрого превью хватает 4-8.
Кстати, в Step 3.5 Flash была похожая ситуация: модель могла галлюцинировать при tool calls, если неправильно настроить количество шагов. Там проблема была в стабильности, но решение тоже лежало через настройку параметров, а не через хардкод.
Я решил поэкспериментировать: запустил один и тот же промпт с 8, 28, 50 и 100 шагами. Разница — колоссальная. 8 — шумно, 28 — приемлемо, 50 — детализированно, 100 — почти фотореалистично, но время генерации растёт линейно. И никакого «срыва».
Деплой с граблями: как мы откатывали фикс
Исправление заняло пять минут. Я вынес количество шагов в переменную окружения FLUX_INFERENCE_STEPS с дефолтом 28. Потом добавил опцию в API. Протестировал — всё работает. Залил в прод.
Но на этом история не кончилась. Через день после деплоя я заметил, что некоторые пользователи начали ставить 100 шагов, и сервак лёг под нагрузкой. Пришлось добавить лимит сверху (максимум 100) и квоты. И вот тут мы снова задумались: а не перестраховываемся ли мы опять?
Но это уже другая история. Главное — первоначальный баг был именно в самоограничении. Как в баге с преждевременным EOS в GLM-4.5-Air, где модель обрывала генерацию раньше времени, потому что разработчики так настроили токенайзер. Только у нас наоборот — мы искусственно обрывали генерацию, не давая модели дойти до лучшего качества.
Почему мы так делаем? Психология самоограничения
Я задумался: а почему я, опытный инженер, вдруг решил, что 28 — это максимум? Ведь буквально везде пишут про adjustable steps. Ответ: страх. Страх, что если дать пользователю слишком много свободы, он сломает систему. Страх, что модель поведёт себя непредсказуемо. Но в итоге мы ломаем собственную гибкость.
Вспомнилась статья про Mistral 4 в GGUF, который врал о контексте в 1 млн токенов. Там тоже разработчики приняли заявленное «1 млн» как факт, а на деле модель работала только с 32k. Ошибка интерпретации документации — наша общая проблема.
Совет: Если в документации написано «default: 28», это не значит «recommended: 28» или «max: 28». Default — это просто значение по умолчанию. Всегда проверяйте, можно ли его менять, и если да — делайте параметры настраиваемыми.
Пошаговый план исправления (чтобы вы не наступили на те же грабли)
1 Аудит кода на хардкод
Найдите все вхождения num_inference_steps, max_length, max_tokens и других «магических чисел». Проверьте, не зашиты ли они константами.
2 Вынесите в конфиг
Используйте переменные окружения, yaml-конфиги или хотя бы константы с комментарием «можно менять». Для Python: NUM_STEPS = int(os.getenv("FLUX_STEPS", 28))
3 Протестируйте с разными значениями
Проверьте, что модель не ломается при минимальных (1-4) и максимальных (100+) шагах. Снимите метрики качества (FID, CLIP score) для разных значений. Убедитесь, что нет побочных эффектов.
4 Добавьте документацию для пользователей
Объясните, что больше шагов = лучше качество, но дольше; меньше шагов = быстрее, но шумнее. Дайте рекомендации (например, 8-12 для превью, 28-50 для продакшна, 100+ для премиум).
Чему я научился на этой ошибке
Во-первых, не читать документацию по диагонали. Во-вторых, всегда подозревать себя в самоограничении. Если вы думаете «это значение нельзя менять» — проверьте, так ли это на самом деле. Скорее всего, можно.
В процессе разбора этого бага я наткнулся на статью про настройку MiMo-2.5 для кодинга. Там проблема была в зацикливании модели, и её решили корректировкой параметров генерации, а не хардкодом. Общий паттерн: сначала «защищаем» систему, потом страдаем от негибкости.
Ещё один пример — баг с зацикливанием GLM-4.7-Flash в LM Studio. Там тоже пришлось копаться в настройках, а не верить дефолтам. Вывод: дефолты — это не догма.
Что пошло не так в моей архитектуре: взгляд DevOps
Как Senior DevOps, я должен был предусмотреть гибкость на уровне инфраструктуры. Параметры инференса — это часть конфигурации сервиса, они должны быть изменяемы без пересборки. Мы это исправили, но осадочек остался.
Если вам интересны похожие кейсы, почитайте сравнение Qwen3.5 для генерации баг-репортов. Там тоже были неожиданные результаты из-за разных настроек генерации. Или как Gemini 2.5 Pro починил 9-летний баг в far2l — отличная демонстрация того, как свежий взгляд помогает найти глупую ошибку.
И, конечно, ловушка Langfuse — ещё один пример, когда дефолтное поведение SDK может навредить. Не доверяйте умолчаниям.
Вместо заключения: парадокс контроля
Чем больше мы контролируем параметры, тем менее гибкой становится система. Я перестал бояться давать пользователям ручки настройки. Да, они могут ошибиться. Но лучше они ошибутся, чем мы решим за них, что им нужно.
Сейчас в нашем сервисе можно выбирать количество шагов от 8 до 100. И — о чудо — никто не сломал генерацию. Наоборот, пользователи рады, что могут ускорить или улучшить качество под свою задачу.
Так что, если вы тоже хардкодите дефолты — остановитесь. Выпустите параметр наружу. Модель вас не укусит. А если укусит — ну, тогда хотя бы честно.