happyin.work / blog

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

Qué hacen 100 nodos personalizados de ComfyUI en producción y cómo se distribuyen

Autora: Anastasiia Butova, ingeniera de ComfyUI y modelos de difusión, Belgrado · publicado 2026-09-23

Traducción de la versión en inglés: English

He escrito 100 nodos personalizados de ComfyUI en 16 paquetes. El número se contó el 15 de septiembre de 2026 a partir del código de registro de cada paquete, no de memoria. La mayoría son una de tres cosas: un modelo envuelto para que corra dentro del grafo, una decisión que el grafo tiene que tomar, o una operación de imagen que ningún paquete existente hace como nuestro pipeline necesita. Abajo hay ejemplos reales de cada una, y después la parte que suele saltarse: cómo entra un nodo en una imagen de producción, y qué pasa cuando el nodo equivocado llega a un servidor.

De dónde sale el número

Un script lee los fuentes Python de cada paquete y recoge los ids de nodo en las tres formas de registro que acepta ComfyUI. Encontró 104 registros y 100 nodos distintos, ya que cuatro nodos de utilidad están registrados por dos paquetes; si alguna fuente no puede leerse, sale con código 2 y el recuento no se publica. Los paquetes más grandes son uno general con 32 nodos (enrutado, compuertas, máscaras, ruido, color, hojas de contactos), un paquete de gemas con SAM 3 con 20 y un paquete de teselado y frecuencias con 19. Diez paquetes pequeños tienen de uno a tres nodos cada uno, sobre todo envoltorios de un solo modelo. La tabla completa está en /comfyui/.

Los repositorios son privados, y varios paquetes se escribieron dentro de trabajos de clientes y de empleadores (la descripción del repositorio del paquete más grande todavía dice "by Glam AI"), así que los describo en lugar de enlazarlos. Antes de este recuento, la web decía "más de 100 nodos, de código abierto". El número se sostuvo; "de código abierto" no.

Un modelo, envuelto para que corra dentro del grafo

NAFNet, SwinIR y HYPIR restauran, SAM 3 segmenta piezas de coche y gemas, DINOv3 mide coches y clasifica, YOLO26 detecta. Como nodos, corren en el mismo trabajo que la generación, en la misma GPU, sin un viaje de ida y vuelta a otro servicio.

Llamar al modelo es lo más fácil de un envoltorio. Lo demás es hacer que se comporte en un hardware que el autor del grafo nunca vio. El nodo SwinIR tiene perfiles de GPU que fijan el tamaño de tesela y el lote, y el JSON del flujo llevaba el perfil que eligió su autor, por ejemplo H200 con 141 GB. En serverless de RunPod, el planificador entrega lo que tenga el pool del endpoint, desde varias variantes de H100 hasta una RTX PRO 6000 Blackwell. El perfil equivocado significaba errores de memoria, reintentos y trabajos que superaban el timeout de 30 minutos. Desde la versión 1.6.0, el nodo detecta la tarjeta por sí mismo e ignora ese ajuste.

También solía caer a la CPU cuando se quedaba sin memoria, donde una tesela tarda unos 40 segundos y un trabajo completo horas. En la 1.5.8 hice quitar esa caída: ahora el nodo falla de forma ruidosa con un resumen de memoria. Un éxito lento que nadie nota es peor que un error que alguien arregla.

Decisiones que el grafo tiene que tomar

Un grafo de ComfyUI no tiene if. Los nuestros se ramifican así. Un nodo enrutador pone la imagen real en la salida que eligió y un marcador negro de 1×1 en todas las demás. Al inicio de cada rama hay una compuerta:

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 le dice a ComfyUI que no ejecute nada aguas abajo, así que una rama no elegida no cuesta nada de GPU. Al final de cada rama, un nodo marcador con una entrada perezosa, o bien pide el resultado de su rama, o, si su rama no fue elegida, deja pasar el 1×1 sin pedir nada; un nodo de fusión entrega entonces la primera imagen real. Sin la entrada perezosa, el bloqueador llegaría a la fusión y bloquearía todo lo que viene después.

Tres de los enrutadores:

  • NSFW y degradación. Una marca NSFW más un veredicto de calidad a partir de métricas de píxel (bloques de JPEG desde la DCT, bandas de color, ruido de crominancia, ringing, desenfoque) y una puntuación estética basada en CLIP. Cuatro salidas de imagen, desde nsfw_bad_quality hasta safe_good_quality, para que un fotograma degradado vaya a una rama de escalado y uno limpio a otra más ligera.
  • Primer plano. La cobertura del rostro se mide de dos formas, con segmentación de piel facial de MediaPipe y con el recuadro de un detector de rostros; gana la señal más alta.
  • Enrutador de prompts de Florence. En un flujo de retoque de joyería, Florence-2 describe el fotograma, y si la descripción nombra una piedra, se usa el prompt de gemas.

El enrutador de Florence es del que más aprendí. En 30 fotogramas reales de clientes, Florence nombró las piedras en los 18 fotogramas que las tenían, y aun así la rama de piedras se eligió solo 17 veces. Las tres causas estaban en el emparejamiento, ninguna en el modelo. Plurales: diamonds no coincide con \bdiamond\b. Negaciones: "There are 0 gemstones" contiene el sustantivo. Formas: "diamond-shaped design" es un patrón, no una piedra. La solución obvia, una s opcional, fue peor: se disparó en los 30 fotogramas, porque las frases negadas también contienen el sustantivo. El orden importa: quitar negaciones, quitar usos de forma y luego emparejar con plurales. Y "cut" no podía ir a la lista de formas, porque "brilliant-cut stone" nombra una piedra real.

Sobre un mismo conjunto de 22 descripciones, el emparejamiento antiguo acertó 13, una primera corrección propuesta 17 y la versión final 22. La parte que conservaría en cualquier enrutador: el emparejamiento salió del nodo y pasó a una función simple, probada con descripciones guardadas, sin Florence y sin GPU. Como dice el mensaje del commit, "una regresión ahora cuesta un segundo en lugar de una ejecución del grafo".

Operaciones de imagen que ningún paquete hace a nuestra manera

El tercer tipo es procesamiento de imagen simple, donde los nodos existentes hacían algo parecido pero no lo que el pipeline necesitaba:

  • Ruido. Un estimador (Laplaciano más MAD sobre la décima parte más lisa de parches de 64×64, menos un pequeño suelo para que una imagen limpia no parezca ruidosa) y un igualador de grano que traslada el grano de una imagen de referencia a la imagen objetivo.
  • Alineación. Alineación por flujo óptico con un respaldo de deformación, y luego mezcla de frecuencias, para que dos versiones de un fotograma se alineen antes de mezclarse.
  • Tamaños. Un ajuste de tamaño del latente que estira el fotograma hasta el tamaño más cercano de la tabla del modelo elegido, con cada lado múltiplo de 16, en lugar de recortarlo.
  • Teselado. División y fusión de teselas y descomposición de frecuencias para fotos de producto de 6K–10K píxeles; las costuras que trae el teselado están en un post aparte.
  • Coches. Para mashinki, un nodo que inclina la placa de fondo para igualar el balanceo de la cámara de la cabina.

Cómo entra un nodo en una imagen de producción

Para OIS.Gold, una imagen de producción parte de un paquete vendorizado: 21 paquetes, 14 públicos y 7 privados, copiados sin sus carpetas .git. La construcción escribe el commit del paquete en la imagen como .vendor-sha, así que una imagen puede decir de qué paquete se construyó. Actualizar es deliberado: refrescar los paquetes, ejecutar los flujos contra el nuevo estado, confirmar, reconstruir. Para un paquete vendorizado, un cambio en la rama principal de otra persona no puede llegar a producción de la noche a la mañana.

El paquete se armó el 23 de abril, después de una pérdida. Cuatro días antes se había terminado un host de GPU, y la instantánea de su contenedor contenía la imagen exterior pero no los nodos personalizados. Un nodo, el enrutador de Florence de arriba, se perdió con ella. Lo reconstruí el 27 de abril a partir de sus entradas y salidas tal como estaban registradas en el JSON del flujo de producción.

La regla de vendorizado tiene una excepción, y prefiero decirla en lugar de redondearla. Los paquetes en desarrollo activo, tres míos y algunos de terceros en la imagen de Blackwell, se clonan de nuevo sobre el paquete en el momento de construir desde su rama principal, con un pin de commit opcional para cuando una rama sufre una regresión. Para esos paquetes, la rama principal es producción, incluida la de otra persona. Por eso mi paquete más grande tiene CI desde agosto, y el archivo del flujo lo dice en una línea:

Lo que llegue a master llega a la siguiente reconstrucción de workers, así que master tiene que seguir en verde.

Las pruebas no instalan torch a propósito. Cubren lo que corre sin Florence y sin GPU, que es exactamente donde estaban los errores del enrutador.

Probar no es juzgar. Un nuevo punto de control o un cambio de nodo se juzga en una hoja frente a la versión anterior, con los mismos prompts congelados y la misma semilla en cada casilla. La página de wowfaceaibot muestra dos hojas de este tipo, de mis propias pruebas de LoRA de personaje.

Un paquete se ejecuta cuando se importa

Dos módulos de mi paquete más grande nunca fueron nodos. No tenían entradas ni registro y existían solo por sus efectos secundarios al importarse: uno envolvía PromptExecutor.execute de ComfyUI para guardar el PNG y el JSON del flujo de cada prompt nuevo, y el otro parcheaba la ruta de guardado para anteponer el nombre del contenedor. Útil en una máquina de desarrollo. En producción serverless, los workers efímeros escribían un historial que nadie leería, y un envoltorio con su propio try/except alrededor del ejecutor podía ocultar un error real del flujo. En mayo se trasladaron a un repositorio aparte que la construcción de producción nunca clona, y cualquier efecto secundario permanente vive allí desde entonces.

El mismo enrutador de Florence enseñó la otra mitad. Cargaba Florence-2 con trust_remote_code=True, así que en un worker nuevo la biblioteca transformers descargaba el Python del modelo desde el Hub de Hugging Face y lo ejecutaba. En mayo ese código dejó de funcionar con la versión instalada de transformers. El enrutador capturó el error y devolvió una descripción vacía, y la cascada terminó con workers que entregaban un PNG plano en lugar del PSD por capas con el que trabajan los retocadores. La solución apuntó a nuestro propio nodo: una copia local cargada con local_files_only, y transformers fijado. Un HF_HUB_OFFLINE=1 global duró un día, porque también bloqueaba una descarga legítima de un nodo de la comunidad. En agosto, en una máquina con transformers 5, el nodo pasó a usar el código del modelo dentro del paquete ComfyUI-Florence2, que nunca toma la ruta de código remoto. Ese segundo fallo había estado oculto durante días detrás de la misma descripción vacía.

Un nodo personalizado es Python con los permisos del servidor

Este es el incidente detrás de la línea de seguridad de /comfyui/. En febrero de 2026, un contenedor de ComfyUI en un servidor con H100 en el que trabajé estaba minando Monero con una variante de XMRig; el host en sí estaba limpio.

El puerto de la API de ComfyUI del contenedor estaba expuesto a internet sin autenticación. Alguien envió un flujo que usaba SRL Eval de srl-nodes, un paquete público. El nodo hace lo que dice su docstring: evalúa cualquier Python que le des, y un comentario en su código añade "ComfyUI isn't secure to begin with". En una máquina cerrada eso es una comodidad. En un puerto abierto es un shell remoto.

El código del atacante reescribió /opt/ComfyUI/execution.py: el archivo limpio tiene unas 1.282 líneas, y las copias infectadas llegaban a 1.700–2.100 y más. En cada ejecución de un flujo, el código inyectado pausaba el minero para que la generación tuviera la CPU, y luego lo reanudaba, y volvía a descargar el binario si se había borrado. Los binarios estaban en unos 27 directorios con nombres que parecen servicios del sistema, distintos en cada infección: tres oleadas en cinco días, los días 13, 17 y 18 de febrero. Borrar el paquete no bastó, porque ComfyUI-Manager guardaba una instantánea de los nodos instalados, veía al reiniciar que srl-nodes faltaba y lo volvía a clonar. La reinfección llegó en minutos.

Escribí el runbook de detección y eliminación, con un script de comprobación y un script de limpieza. El orden es lo importante:

  1. Bloquear primero el puerto desde fuera, en la cadena DOCKER-USER de iptables, o todo lo de abajo se deshace en minutos. Las reglas no sobreviven a un reinicio del demonio de Docker a menos que se guarden.
  2. Matar cada proceso del contenedor excepto el propio ComfyUI.
  3. Borrar el paquete y cada instantánea de Manager que lo liste.
  4. Borrar los binarios, y luego buscar cualquier ejecutable que quede fuera de los directorios de herramientas conocidos.
  5. Restaurar execution.py desde git, la única forma fiable, y luego comprobar su número de líneas y que no haya coincidencias con las marcas del minero.
  6. Reiniciar, confirmar que el paquete no ha vuelto y hacer commit de la imagen limpia.

El script de comprobación mira cinco cosas: cadenas del minero en execution.py, procesos extra, ejecutables inesperados, conexiones al pool y si el paquete ha regresado. El runbook también enumera lo que no debe borrarse: un paquete desconocido en ese contenedor era una gestión de memoria de GPU legítima.

Releyendo el script de comprobación para este post, encontré un fallo. Si SSH no puede conectar, todos los recuentos llegan vacíos, cada prueba cae en OK, y el script imprime RESULT: CLEAN. Una comprobación que no pudo ejecutarse tiene que decirlo como un tercer resultado.

En abril de 2026, Censys publicó un análisis de una campaña contra ComfyUI expuesto a internet. SrlEval está en su lista de nodos explotados, y cuando no hay ningún nodo vulnerable instalado, el escáner instala uno por su cuenta a través de ComfyUI-Manager. El mismo nodo, y el Manager de nuevo como instalador.

Qué significa esto para escribir y distribuir nodos

  • Las entradas de un nodo son una API. Si un nodo ejecuta código, lee una ruta o descarga algo a partir de sus entradas, quien controla el grafo controla eso.
  • ComfyUI se mantiene fuera de internet público, detrás de un servicio estrecho; cómo funciona eso para el estudio de coches de esta web está en el post complementario.
  • Los nodos llegan en el momento de construir, desde una lista en git, y un pin gana a una rama. El código que llega en tiempo de ejecución, una instantánea que reinstala un paquete o trust_remote_code que descarga Python, es código que nadie distribuyó a propósito.
  • Cuando producción se construye desde una rama, esa rama tiene CI, con las pruebas sin GPU dentro del paquete.

Si necesitas un nodo para un paso que ningún paquete existente cubre, o un pipeline hecho seguro para distribuir, cómo trabajo está en /comfyui/.

Más artículos

En otras partes del sitio

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