本番で100個のカスタム ComfyUI ノードは何をしているか、そしてどう出荷しているか
著者: Anastasiia Butova, ComfyUI・拡散モデルエンジニア(ベオグラード) · 公開 2026-09-23
英語版からの翻訳: English
私は16パックにまたがる、100個のカスタム ComfyUI ノードを書いてきました。この数は、2026年9月15日に、各パックの登録コードから数えたものです。覚えていたものではありません。そのほとんどは、次の3種類のどれかです。グラフの中で動くようにラップされたモデル、グラフが下さなければならない判断、既存のどのパックもパイプラインに必要な形では行っていない画像処理です。以下に、それぞれの本物の例を挙げ、その後に、普通は飛ばされる部分を書きます。ノードが本番のイメージにどう入るか、そして間違ったノードがサーバーに入ったらどうなるかです。
数字はどこから来たか
スクリプトが、各パックの Python のソースを読み、ComfyUI が受け付ける3つの登録の形からノード ID を集めます。104件の登録と、100個の異なるノードが見つかりました。4つのユーティリティノードは2つのパックで登録されているからです。どれかのソースが読めなければ、終了コード2で終わり、その数は公開されません。最大のパックは、32ノードの汎用パックです(ルーティング、ゲート、マスク、ノイズ、色、コンタクトシート)。次は、20ノードの SAM 3 の宝石パック、19ノードのタイリングと周波数のパックです。小さな10個のパックには、それぞれ1〜3ノードずつ入っていて、大半は単一モデルのラッパーです。完全な表は /comfyui/ にあります。
リポジトリは非公開で、いくつかのパックは、クライアントや雇用先の仕事の中で書かれました(最大のパックのリポジトリの説明には、今も「by Glam AI」とあります)。そのため、リンクはせず、説明だけにします。この数え上げの前、サイトには「100+ ノード、オープンソース」と書かれていました。数の部分は正しかったのですが、「オープンソース」は正しくありませんでした。
モデルを、グラフの中で動くようにラップする
NAFNet、SwinIR、HYPIR は復元、SAM 3 は車の部品と宝石のセグメンテーション、DINOv3 は車の計測と分類、YOLO26 は検出を行います。これらはノードとして、生成と同じジョブ内で、同じ GPU 上で、別のサービスへの往復なしに動きます。
ラッパーを書くとき、モデルを呼び出すのは簡単な部分です。残りは、グラフの作者が一度も見たことのないハードウェアで、正しく動くようにすることです。SwinIR ノードには GPU プロファイルがあり、タイルのサイズとバッチを設定します。ワークフローの JSON には、作者が選んだプロファイルが入っていました。たとえば H200 の 141 GB です。RunPod のサーバーレスでは、スケジューラーはエンドポイントのプールにあるものを割り当てます。H100 のいくつかの変種から RTX PRO 6000 Blackwell まで、さまざまです。合わないプロファイルは、メモリ不足のエラー、リトライ、そして30分のタイムアウトを超えるジョブを生みました。1.6.0 からは、ノードがカードを自分で検出して、その設定を無視します。
以前は、メモリ不足のときに CPU へフォールバックしていました。CPU では、1タイルに約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 を次へ渡します。そして、マージノードが最初の本物の画像を次へ渡します。遅延入力がなければ、ブロッカーがマージに届き、その後ろのすべてをブロックしてしまいます。
ルーターのうち3つ:
- NSFW と劣化。 NSFW のフラグと、ピクセルの指標からの品質の判定(DCT によるJPEGのブロック状のノイズ、色のバンディング、クロマのノイズ、リンギング、ぼけ)、そして CLIP ベースの美的スコア。画像の出力は4つあり、
nsfw_bad_qualityからsafe_good_qualityまでです。そのため、劣化したフレームはアップスケールのブランチへ、きれいなフレームは軽いブランチへ行けます。 - クローズアップ。 顔の占める割合を2通りで測ります。MediaPipe の顔の肌のセグメンテーションと、顔検出器のボックスです。強い信号のほうが採用されます。
- Florence のプロンプトルーター。 宝石のレタッチのワークフローでは、Florence-2 がフレームにキャプションを付けます。キャプションに石の名前があれば、宝石のプロンプトが使われます。
Florence のルーターは、私が最も多くを学んだものです。実際のクライアントのフレーム30枚のうち、Florence は石のあるフレーム18枚すべてで石の名前を挙げました。それでも石のブランチが選ばれたのは17回だけでした。原因は3つとも、マッチングにあり、モデルにはありませんでした。複数形、diamonds は \bdiamond\b にマッチしません。否定、「There are 0 gemstones」は名詞を含みます。形状、「diamond-shaped design」は模様であり、石ではありません。明らかな修正、語尾の任意の s は、もっと悪いものでした。30枚すべてで誤って発火したのです。否定された文にも名詞が含まれるからです。順序が重要です。否定を取り除き、形状の用法を取り除き、その後で複数形を含めてマッチさせます。また、「cut」は形状のリストに入れられませんでした。「brilliant-cut stone」は実際の石を指しているからです。
共有の22個のキャプションのセットで、古いマッチングは13件を正しく扱い、最初に提案した修正は17件、最終版は22件でした。どのルーターでも残したい部分はこれです。マッチングをノードの外の、単純な関数へ移し、Florence なしで、GPU なしで、保存されたキャプションを使ってテストできるようにしたこと。コミットメッセージにあるとおり、「回帰は、グラフの実行ではなく、1秒のコストになった」のです。
既存のパックでは、私たちのやり方で行えない画像処理
3つ目の種類は、普通の画像処理です。既存のノードでも近いことはできましたが、パイプラインに必要なものとは違いました。
- ノイズ。 推定器(64×64のパッチのうち最も滑らかな10分の1に対する Laplacian と MAD、そこから小さな下限を引いて、きれいな画像がノイジーに見えないようにしたもの)と、参照画像の粒子を対象に移す粒子のマッチャー。
- 位置合わせ。 オプティカルフローによる位置合わせと、変形のフォールバック、そして周波数の混合。フレームの2つのバージョンを、ブレンドの前に揃えるためです。
- サイズ。 ラテントのサイズを合わせるノードは、選んだモデルのテーブルにある、最も近いサイズへフレームを伸ばします。各辺は16の倍数で、切り抜くのではありません。
- タイリング。 6K〜10K ピクセルの商品写真のための、タイルの分割と結合、周波数の分解。タイリングに伴う継ぎ目は別の記事で扱っています。
- 車。 mashinki 向けのノードで、キャビンのカメラのロールに合わせて背景のプレートを傾けます。
ノードが本番イメージに入るまで
OIS.Gold では、本番のイメージは、ベンダリングされたバンドルから始まります。14個の公開パックと7個の非公開パック、合計21パックで、.git フォルダを除いてコピーされています。ビルドは、バンドルのコミットをイメージの中の .vendor-sha に書き込むので、イメージがどのバンドルからビルドされたかを言えます。更新は意図的に行います。パックを更新し、新しい状態に対してワークフローを実行し、コミットして、再ビルドします。ベンダリングされたパックでは、他人のメインブランチへの変更が、一晩で本番に届くことはありません。
バンドルは4月23日にまとめられました。ある損失の後です。その4日前に GPU のホストが終了され、そのコンテナのスナップショットには、外側のイメージは入っていましたが、カスタムノードは入っていませんでした。そのうち1つは、前述の Florence のルーターで、それも一緒に失われました。4月27日に、本番のワークフロー JSON に記録された入力と出力から、それを作り直しました。
ベンダリングのルールには例外があり、それを丸めて言わずにおくより、はっきり書いておきたいと思います。現在活発に開発されているパック(私のものが3つと、Blackwell のイメージにあるサードパーティのいくつか)は、ビルドのときに、バンドルの上に、メインブランチから改めてクローンされます。ブランチでリグレッションが起きたときのために、任意でコミットを固定することもできます。これらのパックでは、メインブランチが本番です。他人のものも含めてです。だからこそ、私の最大のパックは8月に CI を持つようになりました。ワークフローのファイルが、それを1行で書いています。
master に入ったものは、次のワーカーの再ビルドで本番に届くので、master は緑であり続けなければなりません。
テストは、意図的に torch をインストールしません。Florence なしで、GPU なしで動くものを対象にしています。ルーターのバグがあったのは、まさにその場所です。
テストは、判定とは違います。新しいチェックポイントやノードの変更は、前のバージョンに対して、シートの上で判定します。同じ凍結されたプロンプトと、すべてのスロットで同じシードを使います。wowfaceaibot のページには、私自身のキャラクター LoRA の実行から得た、この種のシートが2枚あります。
インポートされたとき、パックが動く
私の最大のパックには、ノードではなかった2つのモジュールがありました。入力も登録もなく、インポートされたときの副作用のためだけに存在していました。1つは、ComfyUI の PromptExecutor.execute をラップして、新しいプロンプトごとに PNG とワークフローの JSON を保存していました。もう1つは、保存パスにパッチを当てて、コンテナの名前をプレフィックスとして付けていました。開発用のマシンでは役に立ちました。しかし、サーバーレスの本番では、使い捨てのワーカーが、誰も読まない履歴を書いていました。さらに、実行器の周りに独自の try/except を持つラッパーは、本物のワークフローのエラーを隠すことがありました。5月に、それらは本番のビルドがクローンしない、別のリポジトリへ移されました。常時オンの副作用は、それ以来そこにあります。
同じ Florence のルーターが、もう1つの半分も教えてくれました。それは trust_remote_code=True で Florence-2 を読み込んでいたので、新しいワーカーでは、transformers ライブラリがモデルの Python を Hugging Face Hub から取得して実行していました。5月に、そのコードは、インストールされた transformers では動かなくなりました。ルーターはそのエラーを捕まえて、空のキャプションを返し、カスケードの結果、ワーカーはレイヤー付きの PSD ではなく、平坦な PNG を届けるようになりました。レタッチする人が作業するのは、PSD だからです。修正は、私たち自身のノードを狙ったものでした。local_files_only で読み込むローカルのコピーと、transformers の固定です。グローバルな HF_HUB_OFFLINE=1 は1日しか持ちませんでした。正当なダウンロードを行っていたコミュニティのノードも止めてしまったからです。8月に、transformers 5 のあるマシンで、そのノードは ComfyUI-Florence2 パックの中のモデルのコードに切り替えました。そのパックは、リモートコードの経路を決して通りません。2つ目の失敗は、同じ空のキャプションの陰に、数日間隠れていたものでした。
カスタムノードは、サーバーの権限を持つ Python である
これは、/comfyui/のセキュリティの一文の背後にあるインシデントです。2026年2月、私が扱っていた H100 のサーバー上の ComfyUI のコンテナが、XMRig の変種で Monero をマイニングしていました。ホスト自体はきれいでした。
コンテナの ComfyUI のポートは、認証なしでインターネットに公開されていました。誰かが、srl-nodes の SRL Eval を使うワークフローを送ってきました。公開されたパックです。そのノードはドキュメントに書かれたとおり、与えられた任意の Python を評価します。ソースのコメントには「ComfyUI isn't secure to begin with」とあります。閉じたマシンでは、それは便利な機能です。開いたポートでは、リモートシェルです。
攻撃者のコードは、ComfyUI 自身の /opt/ComfyUI/execution.py を書き換えていました。クリーンなファイルは約1,282行ですが、感染したコピーは1,700〜2,100行以上に伸びていました。ワークフローが実行されるたびに、注入されたコードはマイナーを一時停止させ、生成が CPU を使えるようにしてから、マイナーを再開しました。バイナリが削除されていれば、再びダウンロードしました。バイナリは、システムサービスのような名前で、およそ27個のディレクトリに置かれていました。その名前は感染のたびに違いました。5日間に3回の波があり、2月13日、17日、18日でした。パックを削除しても効果はありませんでした。ComfyUI-Manager がインストール済みのノードのスナップショットを保持していて、再起動時に srl-nodes がないことに気づき、それをクローンし直したからです。再感染は数分以内に起きました。
私は、検出と削除のランブックを書きました。チェックのスクリプトと、クリーンアップのスクリプトがあります。順序が肝心です。
- まず外部からのポートを、
DOCKER-USERの iptables チェーンでブロックします。さもないと、以下のすべてが数分で元に戻ります。そのルールは、Docker デーモンを再起動すると、保存しない限り消えます。 - ComfyUI 自身以外の、コンテナ内のすべてのプロセスを停止します。
- パックと、それを載せているすべての Manager のスナップショットを削除します。
- バイナリを削除し、その後、既知のツールのディレクトリの外に残った実行可能ファイルを探します。
execution.pyを git から復元します。これが唯一確実な方法です。その後、行数を確認し、マイナーのマーカーがゼロ件であることを確認します。- 再起動し、パックが戻っていないことを確認して、クリーンなイメージをコミットします。
チェックのスクリプトは5つのことを見ます。execution.py の中のマイナーの文字列、余分なプロセス、想定外の実行可能ファイル、プールへの接続、そしてパックが戻っているかどうかです。ランブックには、削除してはいけないものも書かれています。そのコンテナにあった見慣れないパッケージの1つは、正当な GPU のメモリ管理でした。
この記事のためにチェックのスクリプトを読み直したところ、欠陥が見つかりました。SSH が接続できないと、すべての数が空になり、すべてのテストは OK に流れ、スクリプトは RESULT: CLEAN と表示していました。実行できなかったチェックは、3つ目の結果として、そうはっきり言わなければなりません。
2026年4月、Censys は、インターネットに公開された ComfyUI を狙ったキャンペーンの分析を公開しました。SrlEval は、悪用されたノードのリストにあります。脆弱なノードがインストールされていないとき、スキャナーは ComfyUI-Manager を通じて、自分でそれをインストールします。同じノードで、インストーラーも再び Manager です。
ノードを書いて出荷することについての意味
- ノードの入力は API です。ノードがコードを実行したり、入力に基づいてパスを読んだり、何かをダウンロードしたりするなら、グラフを制御する人が、そのノードを制御します。
- ComfyUI はインターネットから遮断しておいてください。狭いサービスの背後に置きます。このサイトのカースタジオで、それがどう動くかは関連記事にあります。
- ノードはビルドのときに、git にあるリストから来るべきです。そして、ブランチよりもピンが勝ります。実行時に来るコード、すなわちパックを再インストールするスナップショットや、Python を取得する
trust_remote_codeは、誰も意図して出荷していないコードです。 - 本番がブランチからビルドするなら、そのブランチには CI が必要です。GPU なしのテストをパックの中に入れて。
どのパックにもない工程のためのノードが必要な場合、あるいは安全に出荷できるパイプラインが必要な場合、私の仕事の進め方は /comfyui/ にあります。