ComfyUIのノードを本当に理解する

 ComfyUIの使い方なんかではなく、ノードの中身について説明していきます。以下のサイトをかなり参考にしています。

モデルのロード系

 まずはモデルのロードについてみていきましょう。

CheckpointLoader

 チェックポイントファイルからModel(UNet)、CLIP(Text encoder)、VAEをロードします。UNetのkey名からyamlファイルを推察しているようです。v_predictionかどうかは最後の重みの値でチェックしているみたいです(意味不明ですね)。

LoRALoader

 LoRAをロードします。patchesという変数に各keyに対応する重み(upやdown等)を保持しておき、KSamplerに入力されると、重みとマージします。そしてCPUに元の重みのバックアップしておき画像出力時に元の重みに戻します。画像生成に対してマージの計算はそこまで大きくないと思うので、複数のLoRAを適用しても生成時間はほとんど変わらなそうですね。

プロンプト系

CLIPTextEncode

 文字列をテキストエンコーダに入れるだけですが、AUTOMATIC1111氏のwebuiとは文法は同じでも中身は全然違うようです。

  • AUTOMATIC1111ではCLIPの隠れ層出力に重み付けをしたあと、元の平均で正規化します。つまりあるトークンを強くしようとすると、他のトークンは弱くなります。

  • ComfyUIでは単に定数倍するのではなく、空文によるCLIP出力を基準に重み付けします。CFGみたいな式ですね。正規化はしないので、重みをつけたトークンのみが影響を受けます。

※どちらにせよトークンに重みをつけるのはテキストエンコーダの出力に対してです。テキストエンコーダのembeddingや隠れ層の途中の段階で重み付けをするなんてこともできるんかね。

 またXLの場合テキスト以外にも画像解像度等の条件が加えられます。CLIPTextEncodeSDXL等で設定することもできますが、CLIPTextEncodeの場合は自動的に決定されます。target_*やoriginal_*は生成画像と同じ解像度で、crop_*は0になります。またRefinerのためのaesthetic_scoreはポジティブ側に6.0、ネガティブ側に2.5を設定しています。この辺りはstabilityAIのオリジナル実装のデモに合わせています。
 さらにXLで使うpooled_outputもここでつくられます。

ConcatConditioning

  AUTOMATIC1111氏のwebuiでいうところのBREAK構文を実装するものです。二つのCLIP出力を結合します。上であげたXLのための解像度情報やpooled_outputは結合できないので、conditioning_toの方を使います。ConditioningAverageというものもあって、こっちは結合ではなく線形補間します。

ConditioningSetMask

 Latent Coupleのためのものです。プロンプトの効果範囲をマスクによって限定することができます。ConditionalCombineと合わせて複数のマスク付きプロンプトを入力するのが基本的な使い方になります。
 画像生成時にはそれぞれのマスクの合計が1になるよう正規化されます。つまり0.1とかのマスクをかけたところで、他に被っている部分がないと1に戻ります。またどのプロンプトにも指定されていない領域がある場合、正規化時に0割りエラーが起こります。
 実装みてないけど、多分Latent Coupleのように複数のプロンプトでノイズ予測をして、マスクをかけた後に合計しているだけです。プロンプトの数だけ計算時間が増えていきます。
 似た機能としてConditioningSetAreaがあります。こちらはUNetに与える入力そのものをその範囲に限定します。領域外のことは全く考えない仕組みになるので、より厳密に分割された画像が生成されます。こちらはそういう意味合いから長方形しか指定できないのでマスクではなく形や位置を指定する方法になっていますね。
 応用例としてMaskにしろAreaにしろあえて画像全体を指定することによって、AND構文と同じことができそうです。まあマイナスの重みをかけられないので完全な互換ではないですが。

Apply ControlNet

 ControlNetの情報はConditioningに与えるようになっています。ModelではなくConditioningに渡すのは、ポジティブとネガティブで設定を分けることができるようにですかね。ControlNet自体はControlNetLoaderによってロードできます。ファイル名の最後を_shuffleにすると、ControlNetの出力がglobal average poolingされます(なんじゃそりゃ)。ちなみにT2i-AdapterもControlNetに含まれています。

潜在変数(VAE)

VAEEncode(Decode)

  ComfyUIのVAEEncode(Decode)では結構特殊な処理が盛りだくさんです。まず画像サイズやバッチサイズからメモリ必要量を推定して、GPUの余剰メモリで計算可能なバッチサイズで処理します。そしてそれでも足りなかった場合、TiledVAEが自動的に読み込まれます。
 TiledVAEは、画像をいくつかのタイルに分けて、それぞれ計算することによってVRAM使用量を削減する方法です。ComfyUIでは、512×512と、1024×256と256×1024の3つのタイルを使います。それぞれの大きさで128ピクセルのオーバーラップでタイル分けします。オーバーラップとは隣同士のタイルが重なり合う領域の大きさのことです。完全に分けてしまうと区切られたような画像が生成されてしまうので、それを防ぐためにタイル同士を重ねあいます。3つのサイズに対してそれぞれタイル分けされた領域をVAEでエンコード(デコード)します。タイルが重なった領域についてはより近いタイルの結果を重視するような重み付け平均をとります。そして最後に3つのサイズ分けした計算結果の平均をとります。
 型についても少し特殊です。ComfyUIは最近--fp16-vaeみたいなオプションが追加されました。これを指定しない場合、bfloat16が使えるGPUはbfloat16を、それ以外のGPUではfloat32を使います。SDXLのVAEを除いてfloat16でNaNが出ることは少ないので、雑魚GPUを使ってる人は--fp16-vaeを指定してみてはどうでしょうか。

VAEEncodeForInpaint

 Inpaint用のエンコーダで、マスクで指定した領域を0.5(灰色)にしたあとエンコードします。denoise=1.0にしても元の画像の情報はわずかに残ってしまうので、灰色にするのだと思いますが、潜在変数の指定領域部分を0にする方がいいんじゃないかとも思ったり。その場合は8ピクセル四方単位になってしまうので微妙なのかな。さらにマスク情報を追加しておき、KSamplerに入れるとマスク以外の領域を元画像のまま維持するようになります(要するにinpaint)。領域を灰色にしたくない場合はVAEEncoderとSetLatentNoiseMaskの二つで同様のことができます。denoise<1にしてある部分を修正するみたいなことをしたいときはこっちを使います。

KSampler

 画像生成全般を行うノードです。このノードだけ規模が大きすぎて何をやっているのかわけわかめです。LoRAやControlNetの適用処理をしたり、conditioningのマスクを処理したり、samplerを指定したりと各ノードがあとはよろしく!とやり残したものを全て任されているので大変です。もう少し分けてくれないかなとも思いますがそれはそれで不便になるんでしょうね。

denoiseとstepsの関係

 まず重要なところから、AUTOMATIC1111氏のimg2imgとdenoise-stepsの関係が異なります。AUTOMATIC1111版ではtxt2imgと同じように0から1000までの時刻を指定されたstepsに区分して、denoising_strengthをかけたstepからノイズ除去を始めます。ComfyUIでは0から1000*denoiseまでをstepsに応じて区分します。
 例として、50step、denoise=0.5とすると、AUTOMATIC1111版では25回のノイズ除去を行います。それに対してComfyUIでは設定そのまま50回になります。そのためComfyUIではdenoise=0.5にするときはstepsも半分にした方がいいです。hires fixを使うときとかに同じstep指定すると計算量がやばいことになりますよ。

scheduler

 時刻をどうやって分割するかを指定します。normalは時刻を等間隔に分割します。simpleやddim_uniformもほとんど同じです。karrasは人の名前で、sigmaという値(簡単に言うとノイズの強さ)の7乗根が等間隔になるよう分割します。意味わかりませんが、normalに比べて中間ステップの精度を落とす代わりに最初と最後らへんの精度をあげているようです。exponentialはSNRの平方根の対数を基準に分割しています。実装を見るとsigma(SNRの平方根の逆数)にlogをつけて等分していますね。これはdpm-solverというサンプラーが時刻ではなくSNRの平方根の対数を基準にしていることからきています。ようするにdpm-solver用ですが他のサンプラーでもエラーは起きません。

sampler

 いろいろあるよ。

  • euler:ODE化した逆拡散過程をオイラー法で解きます。

  • heun:上のheun版です。二回分の計算が必要です。

  • lms:よく知らんが4段の線形多段法を使っているらしい。

  • dpm:時刻ではなく対数平方根SNRを基準にODE化したもの。

  • dpmpp:dpmよりCFGを使ったときの精度が上がる改良版です。

  • dpm-fast:stepsに応じてどのsamplerを使うか自動で決める方法です。

  • dpm-adaptive:stepsを自動で決めます。生成時間がばらばらになる。

  • ddim:ODEソルバーではなくddpmの改良版です。

  • uni_pc:全く分からん

 さらにdpm系にはいろいろ付きます。2, 2sがつくものは二階近似になっていて、二回分の計算が必要です。2mは前ステップの情報を使うことで一回分の計算で二階近似ができるようにしたものになります。ancestralが付くものは少し多めにノイズ除去をした後ノイズを加えます。そのため各ステップで画像がころころかわって収束しないが、なんかいいらしいです。sdeがつくものも同じような意味らしいです(分かってない)。gpuがつくものはsdeがつくものにだけあり、途中で作るノイズをgpuで作るかcpuで作るかという意味っぽいです。生成速度や精度には影響しないと思います。

preview

 起動時に--preview-method autoなどとすると生成時のぷれびゅーがみれます。いくらなんでも画像生成中に何度もVAEデコードを行うわけにはいかないので、簡易版のモデルが用いられます。vae_approxにtaesdを入れていると、自動的にtaesdが使われます。そうでない場合謎の4×3行列で生成します。

Advanced

 KSamplerにはAdvancedバージョンがあります。denoiseでは開始時刻しか指定できませんでしたが、こっちはstart_stepとend_stepで設定できます。ここでの注意点として、start_stepを0、end_stepを15とすると、1~15ステップまでを行います。startの数字は含まれないということですね(1から数えれば)。そのためサンプリングステップを二つに分けたい場合、1個目をstart=0, end=15とした場合、2個目はstart=15, end=30とします。つなげる場合はstartとendを同じ値にせよ!といえばわかりやすいかな。
 他にノイズを最初に加えるかどうかを設定するadd_noiseと最後のステップでいっきに全てのノイズを除去するreturn_with_leftover_noiseがあります。end_stepを指定したstep未満にして、return_with_leftover_noiseをenableにするとノイズを除去しきれていない途中の画像になります。それを次のノードにつなげるときは、既にノイズが含まれているのでadd_noiseはenableにしましょう。つまりサンプリングステップを完全に二つに分けたい場合、1つ目のノードはreturn_with_leftover_noiseをenableにして、2つ目のノードはadd_noiseをdisableしましょう。
 正確につなげてもdpmpp_2mなど過去のステップの情報が必要なサンプラーを使う場合完全再現できないので注意してください。

TomePatchModel

  token mergingを適用するノードです。設定項目はratioだけで、他はtomesdのデフォルト実装に従っています。ratioは上げればあげるほど計算時間が減り精度が落ちるんですが、0.75以上はいくつにしても変わりません。あとSDXLはdownsampleしていない層にTransformersがないため全く効果がありません。この辺の話を詳しく知りたい人は昔記事に書いていますので読んでみてください。