happyin.work / блог

English · Русский · Español · Deutsch · Português (Brasil) · 简体中文 · 日本語

Что делают 100 пользовательских узлов ComfyUI в продакшене и как они доставляются

Автор: Anastasiia Butova, инженер по ComfyUI и диффузионным моделям, Белград · опубликовано 2026-09-23

Перевод английской версии: English

Я написала 100 пользовательских узлов ComfyUI в 16 пакетах. Число посчитано 15 сентября 2026 года по коду регистрации каждого пакета, а не по памяти. Большинство из них относятся к одному из трёх типов: модель, обёрнутая так, чтобы работать внутри графа; решение, которое граф должен принять; или операция над изображением, которую ни один существующий пакет не делает так, как нужно нашему конвейеру. Ниже — реальные примеры каждого типа, а затем та часть, которую обычно пропускают: как узел попадает в продакшен-образ и что происходит, когда не тот узел оказывается на сервере.

Откуда число

Скрипт читает Python-исходники каждого пакета и собирает идентификаторы узлов из трёх форм регистрации, которые принимает ComfyUI. Он нашёл 104 регистрации и 100 различных узлов, потому что четыре вспомогательных узла регистрируются двумя пакетами; если какой-то исходник не читается, скрипт завершается с кодом 2, и число не публикуется. Самые большие пакеты — общий, с 32 узлами (маршрутизация, шлюзы, маски, шум, цвет, контактные листы), пакет SAM 3 для огранённых камней с 20 и пакет тайлинга и частот с 19. Десять маленьких пакетов содержат от одного до трёх узлов каждый, в основном обёртки одной модели. Полная таблица — на /comfyui/.

Репозитории закрытые, и несколько пакетов написаны в рамках работы для клиентов и работодателя (в описании репозитория самого большого до сих пор стоит «by Glam AI»), поэтому я их описываю, а не ссылаюсь. До этого подсчёта на сайте было написано «100+ узлов, open-source». Число выдержало проверку; «open-source» — нет.

Модель, обёрнутая так, чтобы работать внутри графа

NAFNet, SwinIR и HYPIR восстанавливают изображения, SAM 3 сегментирует детали автомобилей и драгоценные камни, DINOv3 измеряет автомобили и классифицирует их, YOLO26 детектирует. Как узлы, они работают в одной задаче с генерацией, на том же GPU, без обращения к другому сервису.

Вызвать модель — самая простая часть обёртки. Остальное — заставить её работать на железе, которое автор графа никогда не видел. У узла SwinIR есть GPU-профили, задающие размер тайла и батч, а JSON воркфлоу хранил тот профиль, который выбрал его автор, скажем H200 с 141 ГБ. На serverless RunPod планировщик выдаёт то, что есть в пуле endpoint-а: от нескольких вариантов H100 до RTX PRO 6000 Blackwell. Неверный профиль означал ошибки нехватки памяти, повторные попытки и задачи, которые выходили за 30-минутный таймаут. Начиная с версии 1.6.0 узел сам определяет карту и игнорирует эту настройку.

Раньше при нехватке памяти узел уходил на CPU, а там тайл занимает около 40 секунд, и полная задача — часы. В 1.5.8 я убрала этот откат: теперь узел громко падает со сводкой по памяти. Тихий медленный успех, который никто не замечает, хуже ошибки, которую кто-то исправит.

Решения, которые должен принять граф

В графе ComfyUI нет if. Наши ветвления устроены так. Узел-роутер выводит настоящее изображение на выбранный выход, а на каждом другом выходе — чёрную заглушку 1×1. В начале каждой ветки стоит шлюз:

def run(self, image):
    from comfy_execution.graph_utils import ExecutionBlocker

    if not torch.is_tensor(image):
        print(f"[HappyinImageGate] NOT a tensor → BLOCK")
        return (ExecutionBlocker(None),)

    h, w = image.shape[1], image.shape[2]

    if h <= 1 or w <= 1:
        print(f"[HappyinImageGate] {w}x{h} → BLOCK")
        return (ExecutionBlocker(None),)

    print(f"[HappyinImageGate] {w}x{h} → PASS")
    return (image,)

ExecutionBlocker говорит ComfyUI не запускать ничего ниже по графу, поэтому ветка, которую не выбрали, не стоит ни секунды GPU. В конце каждой ветки узел-заглушка с ленивым входом либо запрашивает результат ветки, либо, если ветка не выбрана, пропускает 1×1 дальше без запроса; узел слияния затем отдаёт первое настоящее изображение. Без ленивого входа блокатор дошёл бы до слияния и заблокировал всё после него.

Три из роутеров:

  • NSFW и деградация. Флаг NSFW плюс вердикт о качестве по пиксельным метрикам (блочность JPEG из DCT, цветовой бэндинг, шум в цветности, звон, размытие) и оценка эстетики на CLIP. Четыре выхода изображений, от nsfw_bad_quality до safe_good_quality, так что испорченный кадр может уйти в ветку апскейла, а чистый — в более лёгкую.
  • Крупный план. Покрытие лица измеряется двумя способами: сегментацией кожи лица MediaPipe и рамкой детектора лиц; побеждает более сильный сигнал.
  • Роутер промптов Florence. В workflow ретуши ювелирных изделий Florence-2 описывает кадр, и если описание называет камень, используется промпт для драгоценного камня.

Роутер Florence — тот, из которого я извлекла больше всего уроков. На 30 реальных клиентских кадрах Florence назвала камни во всех 18 кадрах, где они были, но ветку с камнями выбрали только 17 раз. Все три причины были в сопоставлении, а не в модели. Множественное число: diamonds не совпадает с \bdiamond\b. Отрицания: «There are 0 gemstones» содержит существительное. Формы: «diamond-shaped design» — это узор, а не камень. Очевидное исправление, необязательная s, оказалось хуже: сработало на всех 30 кадрах, потому что отрицательные предложения тоже содержат существительное. Порядок важен: сначала убрать отрицания, затем убрать употребления в значении формы, и только потом сопоставлять с учётом множественного числа. А «cut» нельзя было ставить в список форм, потому что «brilliant-cut stone» называет настоящий камень.

На одном общем наборе из 22 описаний старое сопоставление дало 13 правильных ответов, первое предложенное исправление — 17, а финальная версия — 22. Часть, которую я бы оставила в любом роутере: сопоставление вынесено из узла в обычную функцию и протестировано на сохранённых описаниях, без Florence и без GPU. Как написано в сообщении коммита, «регрессия теперь стоит секунду вместо запуска графа».

Операции над изображениями, которые ни один пакет не делает нашим способом

Третий тип — обычная обработка изображений, где существующие узлы делали что-то похожее, но не то, что нужно конвейеру:

  • Шум. Оценщик (Лапласиан плюс MAD по самой гладкой десятой части патчей 64×64, минус небольшой порог, чтобы чистое изображение не казалось зашумлённым) и подгонщик зерна, который переносит зерно референсного изображения на целевое.
  • Выравнивание. Выравнивание по оптическому потоку с откатом на деформацию, затем смешивание частот, чтобы две версии кадра совпали перед смешиванием.
  • Размеры. Привязка размера для латента, которая растягивает кадр до ближайшего размера из таблицы выбранной модели, каждая сторона кратна 16, вместо обрезки.
  • Тайлинг. Разбиение и слияние тайлов и частотная декомпозиция для товарных фото 6K–10K пикселей; швы, которые возникают при тайлинге, — в отдельном посте.
  • Автомобили. Для mashinki — узел, который наклоняет фоновую пластину, чтобы совпасть с креном камеры салона.

Как узел попадает в продакшен-образ

Для OIS.Gold продакшен-образ начинается с вендорённого набора: 21 пакет, 14 публичных и 7 приватных, скопированных без папок .git. Сборка записывает коммит набора в образ как .vendor-sha, так что по образу видно, из какого набора он собран. Обновление происходит сознательно: обновить пакеты, прогнать воркфлоу на новом состоянии, закоммитить, пересобрать. Для вендорённого пакета изменение в чужой main-ветке не может попасть в продакшен за ночь.

Набор собрали 23 апреля, после потери. Четырьмя днями раньше хост с GPU был остановлен, и снимок его контейнера содержал внешний образ, но не пользовательские узлы. Один узел, роутер Florence из раздела выше, пропал вместе с ним. Я восстановила его 27 апреля по входам и выходам, записанным в JSON продакшен-воркфлоу.

У правила вендоринга есть исключение, и я предпочитаю назвать его, а не сгладить. Пакеты в активной разработке, три моих и несколько сторонних в образе Blackwell, при сборке клонируются заново поверх набора из их main-ветки, с необязательным закреплением коммита на случай, когда ветка ломается. Для этих пакетов main-ветка — продакшен, в том числе чужая. Поэтому самый большой мой пакет в августе получил CI, и файл воркфлоу говорит об этом одной строкой:

Всё, что попадает в master, доедет до следующей пересборки воркера, поэтому master должен оставаться зелёным.

Тесты намеренно не ставят torch. Они покрывают то, что работает без Florence и без GPU, а именно там и были баги роутеров.

Тестирование — это не оценка. Новый чекпоинт или изменение узла оцениваются по листу в сравнении с предыдущей версией, с одними и теми же зафиксированными промптами и одним сидом в каждом слоте. Страница wowfaceaibot показывает два таких листа из моих собственных прогонов LoRA персонажа.

Пакет выполняется при импорте

Два модуля в моём самом большом пакете никогда не были узлами. У них не было входов и регистрации, они существовали только ради побочных эффектов при импорте: один оборачивал PromptExecutor.execute ComfyUI, чтобы сохранять PNG и JSON воркфлоу для каждого нового промпта, а другой патчил путь сохранения, добавляя имя контейнера в качестве префикса. Полезно на машине разработки. В serverless-продакшене эфемерные воркеры писали историю, которую никто не будет читать, а обёртка со своим try/except вокруг executor-а могла скрыть настоящую ошибку воркфлоу. В мае они переехали в отдельный репозиторий, который продакшен-сборка не клонирует, и любой побочный эффект, который должен работать всегда, с тех пор живёт там.

Тот же роутер Florence научил и второй половине. Он загружал Florence-2 с trust_remote_code=True, поэтому на свежем воркере библиотека transformers забирала Python-код модели из Hugging Face Hub и выполняла его. В мае этот код перестал работать с установленной версией transformers. Роутер поймал ошибку и вернул пустое описание, а цепочка закончилась тем, что воркеры отдавали плоский PNG вместо послойного PSD, с которым работают ретушёры. Исправление нацелили на наш собственный узел: локальная копия, загружаемая с local_files_only, и закреплённая версия transformers. Глобальный HF_HUB_OFFLINE=1 прожил один день, потому что он заблокировал и законную загрузку одного стороннего узла. В августе, на машине с transformers 5, узел перешёл на код модели из пакета ComfyUI-Florence2, который никогда не идёт по пути remote code. Вторая поломка скрывалась за тем же пустым описанием несколько дней.

Пользовательский узел — это Python с правами сервера

Это инцидент, стоящий за строкой о безопасности на /comfyui/. В феврале 2026 года контейнер ComfyUI на H100-сервере, с которым я работала, майнил Monero вариантом XMRig; сам хост оставался чистым.

Порт ComfyUI контейнера был открыт в интернет без аутентификации. Кто-то отправил ему воркфлоу с узлом SRL Eval из srl-nodes, публичного пакета. Узел делает то, что написано в его docstring: выполняет любой переданный Python, а комментарий в исходниках добавляет «ComfyUI isn't secure to begin with». На закрытой машине это удобство. На открытом порту это удалённая оболочка.

Код атакующего переписал /opt/ComfyUI/execution.py: чистый файл — около 1282 строк, зараженные копии были 1700–2100 и больше. На каждом запуске воркфлоу внедрённый код приостанавливал майнер, чтобы генерация получила CPU, а потом запускал его снова и скачивал бинарник заново, если его удаляли. Бинарники лежали примерно в 27 папках под именами, похожими на системные службы, и на каждом заражении имена были другими: три волны за пять дней, 13, 17 и 18 февраля. Удаление пакета не помогало, потому что ComfyUI-Manager хранил снимок установленных узлов, при перезапуске видел, что srl-nodes отсутствует, и клонировал его обратно. Повторное заражение приходило в течение минут.

Я написала runbook обнаружения и удаления с проверочным и очищающим скриптами. Порядок здесь и есть суть:

  1. Сначала закрыть порт снаружи, в цепочке DOCKER-USER в iptables, иначе всё ниже отменяется за минуты. Правила не переживают перезапуск демона Docker, если их не сохранить.
  2. Убить каждый процесс в контейнере, кроме самого ComfyUI.
  3. Удалить пакет и каждый снимок Manager, в котором он указан.
  4. Удалить бинарники, затем поискать любой исполняемый файл вне известных каталогов инструментов.
  5. Восстановить execution.py из git, это единственный надёжный способ, затем проверить число строк и отсутствие маркеров майнера.
  6. Перезапустить, убедиться, что пакет не вернулся, и закоммитить чистый образ.

Проверочный скрипт смотрит на пять вещей: строки майнера в execution.py, лишние процессы, неожиданные исполняемые файлы, соединения с пулом и вернулся ли пакет. Runbook также перечисляет то, что удалять нельзя: один незнакомый пакет в этом контейнере был законным управлением памятью GPU.

Перечитывая проверочный скрипт для этого поста, я нашла в нём дефект. Если SSH не подключается, все подсчёты приходят пустыми, каждый тест проваливается в OK, и скрипт печатает RESULT: CLEAN. Проверка, которая не смогла выполниться, должна говорить об этом как о третьем результате.

В апреле 2026 года Censys опубликовал разбор кампании против ComfyUI, открытых в интернет. SrlEval есть в его списке эксплуатируемых узлов, и если уязвимого узла нет, сканер сам устанавливает его через ComfyUI-Manager. Тот же узел и снова Manager в роли установщика.

Что это значит для написания и доставки узлов

  • Входы узла — это API. Если узел выполняет код, читает путь или что-то скачивает, исходя из своих входов, то тот, кто контролирует граф, контролирует и это.
  • ComfyUI держат вне публичного интернета, за узким сервисом; как это устроено для студии автомобилей на этом сайте, описано в сопутствующем посте.
  • Узлы приходят на этапе сборки из списка в git, и закреплённый коммит лучше ветки. Код, который приходит во время выполнения — снимок, переустанавливающий пакет, или trust_remote_code, который скачивает Python, — это код, который никто не поставлял намеренно.
  • Когда продакшен собирается из ветки, у этой ветки должен быть CI, с тестами без GPU внутри пакета.

Если вам нужен узел для шага, который не покрыт ни одним существующим пакетом, или конвейер, который безопасно выкатить, о том, как я работаю, написано на /comfyui/.

Другие статьи

Ещё на сайте

Anastasiia Butova - ML engineer, Belgrade, Serbia. Email [email protected] · LinkedIn · GitHub · Telegram · Hugging Face