CFG Zero Starとか

最近拡散モデルのCFG(Classifier-Free Guidance)関連にて気になった項目を幾つかメモしておく。また自分なりの考察結果を残しておく。

# 0. CFG(Classifier-Free Guidance)のおさらい
昔の論文ではcfg_scaleはCFGを考えない場合はconditionalを起点としてw=0.0が標準だった。しかし、いつからかCFGを考えない場合はunconditionalを起点としてcfg_scaleは1.0が標準となっている。

https://arxiv.org/pdf/2207.12598

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/5ac5ee2a-e4c4-40fc-ba33-0973f8f7bc6e.png)


$x_{t+1}=\epsilon(x_t,c)+w\cdot(\epsilon(x_t,c)-\epsilon(x_t,uc))$

$x_{t+1}=\epsilon(x_t,uc)+cfg\cdot(\epsilon(x_t,c)-\epsilon(x_t,uc))$

これらは単に係数が1.0違うだけで式としては等しい。cfg_scale=1.0ならw=0.0と等しく$x_{t+1}=\epsilon(x_t,c)$となりcond promptを与えた場合の推論に等しい。
まれにCFGを与えない場合の推論結果を$\epsilon(x_t,uc)$と勘違いされているように思うので一応言及しておく。

また拡散モデル通過後の変化ベクトル$v_{cond},v_{uncond}$で表現してもCFGの数式は変わらない。

$x_{t+1}=x_t+(\epsilon(x_t,uc)-x_t)+cfg\cdot((\epsilon(x_t,c)-x_t)-(\epsilon(x_t,uc)-x_t))$

$(x_{t+1}-x_t)=v_{uncond}+cfg\cdot(v_{cond}-v_{uncond})$



$v_{uncond}=(\epsilon(x_t,uc)-x_t)$
$v_{cond}=(\epsilon(x_t,c)-x_t)$

# 1. CFG Zero Star
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/38b237c3-e227-4977-8b0a-3fcd30cbd1bb.png)


https://arxiv.org/pdf/2503.18886

論文によれば$s^*=(v_{cond}\cdot v_{uncond})/|v_{uncond}|^2$である。
この数式を見た時、自分はこれがcos類似度に似ているように思った。

さて、拡散モデルを通過した時の変化ベクトルがtimestep方向(noise→no noise)とcfg方向(no prompt→prompt)に分解できると考える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/4d3eb066-c03a-4012-b683-562b2d192cc4.png)

$cos \theta =(v_{cond}\cdot v_{uncond})/|v_{cond}||v_{uncond}|$
ここで仮に等式
$|v_{cond}|sin\phi=s|v_{uncond}|sin(\theta+\phi)$が成立するとする。
このとき
$|v_{cond}|sin\phi=s|v_{uncond}|(sin\theta cos\phi+cos\theta sin\phi)$
$s=cos\theta\frac{|v_{cond}|}{|v_{uncond}|}$より

$sin\phi=cos\theta(sin\theta cos\phi+cos\theta sin\phi)$より
$(1-cos^2\theta)sin\phi=cos\theta sin\theta cos\phi$
$tan\phi=\frac{1}{tan\theta}$

つまり$\theta+\phi=\frac{\pi}{2}$となる。
これはsを掛けたuncondと直交する方向にcfg軸が存在すると解釈できる。

また逆に$\theta+\phi=\frac{\pi}{2}$を仮定し、
$|v_{cond}|sin\phi=s|v_{uncond}|sin(\theta+\phi)$が成り立つ係数sを考える時、$sin\frac{\pi}{2}=1$、$sin\phi=cos\theta$であるから係数$s$は$s=cos\theta\frac{|v_{cond}|}{|v_{uncond}|}=(v_{cond}\cdot v_{uncond})/|v_{uncond}|^2$となる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/5a9fa160-df5c-46ca-8bd2-62df2c6ad813.png)

従って$x_{t+1}=\epsilon(x_t,uc)+cfg(\epsilon(x_t,c)-\epsilon(x_t,uc))$のcfgの式にuncondの成分にsを掛けた式を求めると、
$x_{t+1}=s\cdot \epsilon(x_t,uc)+cfg(\epsilon(x_t,c)-s\cdot \epsilon(x_t,uc))$のcfg成分にt方向のベクトル成分を除くことが出来る事が分かる。

# 追記：CFG Zero Starのコード
ComfyUIでは以下のように定義されている。
元のCFGからの差分という形で定義されている。
$\alpha=(ε(x_t,c)-x_t)\cdot (ε(x_t,uc)-x_t)/|ε(x_t,uc)-x_t|^2$
$x_{t+1}=x_{t+1,original}+(\alpha-1) \cdot (1-cfg) \cdot \epsilon(x_t,uc)$ 
これを整理すると以下の通りである。
$x_{t+1}=\epsilon(x_t,uc)+cfg\cdot(\epsilon(x_t,c)-\epsilon(x_t,uc))+(\alpha-1) \cdot (1-cfg) \cdot \epsilon(x_t,uc)$ 
$x_{t+1}=\alpha\cdot \epsilon(x_t,uc)+cfg\cdot(\epsilon(x_t,c)-\alpha\cdot\epsilon(x_t,uc))$
```python
import torch

# https://github.com/WeichenFan/CFG-Zero-star
def optimized_scale(positive, negative):
    positive_flat = positive.reshape(positive.shape[0], -1)
    negative_flat = negative.reshape(negative.shape[0], -1)

    # Calculate dot production
    dot_product = torch.sum(positive_flat * negative_flat, dim=1, keepdim=True)

    # Squared norm of uncondition
    squared_norm = torch.sum(negative_flat ** 2, dim=1, keepdim=True) + 1e-8

    # st_star = v_cond^T * v_uncond / ||v_uncond||^2
    st_star = dot_product / squared_norm

    return st_star.reshape([positive.shape[0]] + [1] * (positive.ndim - 1))

class CFGZeroStar:
...
    def patch(self, model):
        m = model.clone()
        def cfg_zero_star(args):
            guidance_scale = args['cond_scale']
            x = args['input']
            cond_p = args['cond_denoised']
            uncond_p = args['uncond_denoised']
            out = args["denoised"]
            alpha = optimized_scale(x - cond_p, x - uncond_p)

            return out + uncond_p * (alpha - 1.0)  + guidance_scale * uncond_p * (1.0 - alpha)
```

https://github.com/comfyanonymous/ComfyUI/blob/master/comfy_extras/nodes_cfg.py

一方、Kijaiの実装とかでは以下のように最初から差分ベクトル$v_{cond},v_{uncond}$になっている。
ComfyUIでの``alpha = optimized_scale(x - cond_p, x - uncond_p)``のように変化差分の式の変換が見当たらない。


```python
class CFGZeroStarAndInit:
...
    def patch(self, model, use_zero_init, zero_init_steps):
        def cfg_zerostar(args):
            #zero init
            cond = args["cond"]
...
            if (current_step_index <= zero_init_steps) and use_zero_init:
                return cond * 0
                        
            uncond = args["uncond"]
            cond_scale = args["cond_scale"]
                
            batch_size = cond.shape[0]

            positive_flat = cond.view(batch_size, -1)
            negative_flat = uncond.view(batch_size, -1)

            dot_product = torch.sum(positive_flat * negative_flat, dim=1, keepdim=True)
            squared_norm = torch.sum(negative_flat ** 2, dim=1, keepdim=True) + 1e-8
            alpha = dot_product / squared_norm
            alpha = alpha.view(batch_size, *([1] * (len(cond.shape) - 1)))

            noise_pred = uncond * alpha + cond_scale * (cond - uncond * alpha)
            return noise_pred
```


https://github.com/kijai/ComfyUI-KJNodes/blob/main/nodes/model_optimization_nodes.py

多分スケジューラあたりで拡散モデルの出力を変化差分に変換してると思うのだが、厳密にはどうなっているのかは不明。$ε(x_t,c)-x_t→v_{cond}$
```python
model_output = self.convert_model_output(model_output, sample=sample)
...
    def convert_model_output(
        self,
        model_output: torch.Tensor,
        *args,
        sample: torch.Tensor = None,
        **kwargs,
    ) -> torch.Tensor:
...
        # DPM-Solver++ needs to solve an integral of the data prediction model.
        if self.config.algorithm_type in ["dpmsolver++", "sde-dpmsolver++"]:
            if self.config.prediction_type == "epsilon":
                # DPM-Solver and DPM-Solver++ only need the "mean" output.
                if self.config.variance_type in ["learned", "learned_range"]:
                    model_output = model_output[:, :3]
                sigma = self.sigmas[self.step_index]
                alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
                x0_pred = (sample - sigma_t * model_output) / alpha_t
            elif self.config.prediction_type == "sample":
                x0_pred = model_output
            elif self.config.prediction_type == "v_prediction":
                sigma = self.sigmas[self.step_index]
                alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
                x0_pred = alpha_t * sample - sigma_t * model_output
            elif self.config.prediction_type == "flow_prediction":
                sigma_t = self.sigmas[self.step_index]
                x0_pred = sample - sigma_t * model_output
...
            return x0_pred
```

https://github.com/huggingface/diffusers/blob/main/src/diffusers/schedulers/scheduling_dpmsolver_singlestep.py

# 2. CFG Skip
CFGを推論stepの値の条件によってskipする。同じようなのをいくつか見た。

## StepVideo t2v(linear diminishing CFG schedule)

https://arxiv.org/pdf/2502.10248

最初にCFGのtimestepでの変化を見たのは上記の論文だったと思う。
論文中に以下の数式があり、$t>0.11$では$cfg=1.0$になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/7c3e6557-7283-44e3-8ae6-3071daa5b277.png)

この時はふーん、と思いつつ流していたがよく考えると$cfg=1.0$ということはCFGの大きさはゼロという事でNegativePromptの推論をskip出来るという事である。

## CFG Zero Star(Zero-init)
前述の係数sを掛けるのと別にZero-initというのがあるがこれもcfg_skipを示しているのだろうか。デフォルトだとK=1とあるので最初の1stepではCFGを使わないのだろうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/8423a71b-53e1-44b5-83cc-47b909a1f17b.png)



## musubi-tuner(CFG Skip Mode)
https://github.com/kohya-ss/musubi-tuner/blob/main/docs/wan.md#cfg-skip-mode--cfg%E3%82%B9%E3%82%AD%E3%83%83%E3%83%97%E3%83%A2%E3%83%BC%E3%83%89

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/ecc8e471-26f9-4d8f-a374-b64d3ef6e808.png)

いろいろな推論step範囲でのskip方法が実装されている。
lateを推奨しているのを考えると推論後半のstepにおいてcfgをskipしても良いようである。
cfgのコードを見るとskipする時はcond(PositivePrompt)の結果のみで計算する。

```python
    if apply_cfg:
...
        # apply guidance
        noise_pred = noise_pred_uncond + args.guidance_scale * (noise_pred_cond - noise_pred_uncond)
    else:
        noise_pred = noise_pred_cond
```


気になるのはこの定義の仕方とStepVideo t2vの定義で違うのは$t>0.11$となる条件はshift値の大きさに依存する事である。s=7なら推論後半50%skip可能だがs=17では推論後半30%しかskip出来ないのではないか？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/8e4a21e6-55bd-485a-91b8-374e322665d1.png)


```python
import matplotlib.pyplot as plt
import numpy as np

s = 7
s2 = 17
a = 1.57
x = np.arange(51)
y1 = np.arange(51)/50
y2 = 1-(s * (1-y1))/(1+(s-1)*(1-y1))
y3 = 1-(s2 * (1-y1))/(1+(s2-1)*(1-y1))
y4 = np.where(x < 25, 0.004 * x, (x-25+a)*(x-25+a)/(25+a)/(25+a)*(1-0.1)+0.1)

plt.plot(x,y1,label='linear-50step')
plt.plot(x,y2,label='HunyuanVideo shift(s=7.0)')
plt.plot(x,y3,label='HunyuanVideo shift(s=17.0)')
plt.plot(x,y4,label='MovieGen linear-quadratic(250step-smooth)')
plt.hlines(0.11, 0, 50, colors='red', linestyle='dashed')
plt.legend()
plt.show()
```




## Lumina-Image 2.0(CFG-Truncation)
https://arxiv.org/pdf/2503.21758

CFG-Truncationという名で言及されている。
ただ気になるのがcfgをskipする時、cfg_scale=1.0の$v_{cond}$ではなくcfg_scale=0.0の$v_{uncond}$になっている点である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/f6c4dd00-7f81-4717-8fb3-de893be2de09.png)




元論文には以下のような式と図がある。
これもcfg_scale=1.0ではなくcfg_scale=0.0のuncond推論である。
論文内にはT2Iが25%上がるとあるからCFG-Truncを推論の50%に適用できるということだろう。

https://arxiv.org/pdf/2405.15330

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/ee04cd54-7a6f-479c-bcf2-97a438f79f76.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/5dc284b9-074c-4826-ae41-54ba48db03ff.png)

# 3. SkipLayerGuidance
もとはStableDiffusion3.5とかで現れてたらしい。
何故品質が向上するのか自分ははっきりよく分からない。

https://www.reddit.com/r/StableDiffusion/comments/1jac3wm/dramatically_enhance_the_quality_of_wan_21_using/

想像で述べるならCrossAttention計算をskipするのはそのDiTの部分レイヤーにおいては何もtokenを与えない(uncond)の条件に近いのかと思うが、そうした方がuncondとcondのcos類似度よりもskip-layer cond(semi-uncond)とcondのcos類似度が大きい(ベクトル間の角度が小さい)のかと思うくらいだった。


Spatiotemporal Skip Guidance(STG)もこれに何か関連するのかと思ったがよく分からなかった。

https://github.com/junhahyung/STGuidance

https://arxiv.org/pdf/2411.18664

https://www.reddit.com/r/StableDiffusion/comments/1j0lc7v/stg_spatiotemporal_skip_guidance_test_on_wan_t2v/?tl=fr

# 4. image CFG
I2Vモデルで見た。通常のcfgはtxtありとなしでガイダンスを作るが、imgありとなしでのガイダンスを別途考える。
Wan2.1やHunyuanVideoのI2Vではあまり追えてないがこのように多分cfgを二個に分けるようなことはしていないと思われる。

## OpenSora-v2:

https://arxiv.org/pdf/2503.09642v1

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/e0cd618b-3776-463a-8573-d96a6ff501e4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/4d4f0741-7573-4b25-a391-b0f1e1bfc51a.png)

## STIV:

https://arxiv.org/pdf/2412.07730

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/71bc3945-1ecc-4528-8109-dc51692035d6.png)

最良はcfg-T=7.5のcfg-I=1.5らしい。imageのCFGはtextのCFGよりは小さい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/398f9a78-36dd-4a30-b90f-532c961bc49a.png)

## Unconditional Priors Matter:

https://arxiv.org/pdf/2503.20240

二個の条件がある場合の数式は以下のように示される。
この数式も上記と同じである。ただこれはI2Vに限らず他の条件に依存するWan2.1 Fun Controlとかでも同様に示唆される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/2141d281-362b-4c3f-a71c-ab67113444e4.png)

ちなみにこの論文の主題はそこではなく、ファインチューニングモデルはcond推論は良いが、代わりにいくらかuncond推論は劣化しているのでcfgが良好でなく、uncond推論する際はベースモデルのuncond推論を使うべきだとか主張しているようだ。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/2c07ca7d-366a-4db6-b389-94809ec20601.png)
$\epsilon_\psi$がベースモデル、$\epsilon_\theta$がファインチューンモデル。

# 5. CFG-Renormalization

https://arxiv.org/pdf/2412.07730

CFGを与えた推論のベクトル大きさをcond推論の大きさに合わせるように見える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/b3c801a6-c806-464d-b3b2-5951cacebb64.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/a55789f5-d547-4159-9330-59731098ef18.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/964a2a4f-3e62-43da-a7a1-b85af9bf5285.png)

そもそもVAEは画像や動画をガウス分布に変換し、このlatentにガウスノイズを加えてもガウス分布である（分散の大きさの調整は必要だが）。拡散過程やDenoise(Sampling)過程は高次元ガウス球面上の移動とみなせる。

https://qiita.com/ae14watanabe/items/ef5689d40a0fbee957ea



個人的に思う事はCFG自体が単なる線形補外(lerp)であるから二個のベクトルがガウス球面上にある場合、この線形外挿が球面上を飛び出るのは仕方ない。ベクトルの大きさで割って合わせるのではなく球面線形補間(slerp)を使って外挿すればCFG移動がガウス球面上の移動を維持できるのではなかろうか。
Slerpは$\frac{\sin(1-t)x}{\sin(x)}a+\frac{\sin(tx)}{\sin(x)}b$で示せて、これの補外は$t<0$である。$t$が負の整数値を取るとして考える。

https://en.wikipedia.org/wiki/Slerp

|$t<0$   | $\frac{\sin(1-t)x}{\sin(x)}a+\frac{\sin(tx)}{\sin(x)}b$  |
|---|---|
|$\cos(x)$   | $\frac{a\cdot b}{\|a\|\|b\|}$  |
|$\cos(2x)$   | $2\cos^2(x)-1$  |
|$\cos(3x)$   | $4\cos^3(x)-3\cos(x)$  |
|$\cos(4x)$   | $8\cos^4(x)-8\cos^2(x)+1$  |
|$\sin(-x)$   | $-\sin(x)$  |
|$\frac{\sin(x)}{\sin(x)}$   | $1$  |
|$\frac{\sin(2x)}{\sin(x)}$  |$2\cos(x)$  |
|$\frac{\sin(3x)}{\sin(x)}$  |$2\cos(2x)+1=4\cos^2(x)-1$  |
|$\frac{\sin(4x)}{\sin(x)}$  |$2(\cos(x)+\cos(3x))=8\cos^3(x)-4\cos(x)$  |
|$\frac{\sin(5x)}{\sin(x)}$  |$2\cos(2x)+2\cos(4x)+1=16\cos^4(x)-12\cos^2(x)+1$  |
|$\frac{\sin(6x)}{\sin(x)}$  |$2\cos(x)(1+2\cos(4x))=32\cos^5(x)-32\cos^3(x)+6\cos(x)$  |
|$t=-1,\cos x=1.00$   | $2.0a-1.0b$  |
|$t=-2,\cos x=1.00$   | $3.0a-2.0b$  |
|$t=-3,\cos x=1.00$   | $4.0a-3.0b$  |
|$t=-4,\cos x=1.00$   | $5.0a-4.0b$  |
|$t=-5,\cos x=1.00$   | $6.0a-5.0b$  |
|$t=-1,\cos x=0.98$   | $1.96a-1.0b=(1.96a-0.96b)-0.04b$  |
|$t=-2,\cos x=0.98$   | $2.8416a-1.96b=(2.8416a-1.8416b)-0.1184b$  |
|$t=-3,\cos x=0.98$   | $3.60954a-2.8416b=(3.60954a-2.60954b)-0.23206b$  |
|$t=-4,\cos x=0.98$   | $4.23309a-3.60954b$  |
|$t=-5,\cos x=0.98$   | $4.68732a-4.23309b$  |

この表で考えるとslerpはlerpと比べuncond成分をやや引いた線形外挿に近い傾向がある。CFG Zero Starも$v_{uncond}$に係数sをかけるが関連あるだろうか。




# まとめ
正直、cfgは拡散モデルにおいてかなり初期の技術であるから新しい論文などないと思ってたら、今回書いた以外にも調べると最近でもいろいろな論文があって驚いた。