見出し画像

WAN2.2 i2v 14B FP16 GGUF with lightning LoRA 4steps or lightx2v 6steps&Distorch Memory Manager


2025年10月8日、WAN2.2用lightning LoRAを適用したVerⅡ 4stepsを更新しました。

かなり良いです。上のコンセプトはWAN2.1ではなかなか難しい形でした。

About WAN2.2

i2v with WAN2.1 lightx2v LoRA 6steps

i2vⅡ with WAN2.2 lightning LoRA 4steps

いつものやつですね。FP16フルサイズのGGUFを使用しても尚、改造したMulitiGPUノードのDistorchによりVRAM消費量を12GB未満に抑える…動画生成のモデルが出る度に、私の定番になりつつあります。

思ったより速いです。毎度お馴染みDistorchですが、シングルタスク単体は、WAN2.1より速いですね、意外な事に。

また以下の様に、lightx2vによるステップ数削減が効くことも判っています。只、現状WAN2.1用LoRAを使用すると大量のエラーが吐き出される為、そこはスマートではないです。このエラーそのものは不可避ですが、表示自体を「何とかする」事は可能なので後述します。

Distorch改造に関しては、以下を参照して下さい。

480×480だと、処理中は以下の感じです。720×720、length121辺りまではVRAM消費が12GBが収まるようです。上jsonは640×640、length121設定にしています。

fp16 GGUF

量子化していないフルサイズのGGUFを、以下に公開しました。

Ollama-Generate V2

動作は重くなりますが、プロンプトの自動生成機能を付与する為、ollamaを使用します。vision系LLMにも対応しますが、正直動作精度が良くありません。通常のtext生成LLMの方が無難です。

その代わり、プロンプト入力は日本語に対応します。Olama Generaye V2でで自動翻訳がかかる設計にしています。

Fixed lora.py

\ComfyUI\comfy\lora.py"

動作には影響しませんが、本来はWAN2.1用のlightx2v LoRAをWAN2.2に適用すると、大量の

  lora key not loaded: diffusion_model.blocks.0.attn1.to_q.weight
  lora key not loaded: diffusion_model.blocks.0.attn1.to_k.weight

的なエラーが出ます。気分は良くないので、「WAN2.2にWAN2.1用のLoRAを適用した場合」に限り、これを以下の様に変換する修正を施しましたので、上に公開します。

Suppressed 290 WAN2.1 LoRA key warnings (WAN2.1 LoRA on WAN2.2 model - expected behavior)

解説

WAN2.2モデルにWAN2.1 LoRA適用時のみ未ロード警告を集約抑制する実装ノート(ComfyUI `lora.py`)

目的

  • WAN2.2にWAN2.1 LoRAを適用したときだけ、大量に出る「想定内の未ロードキー警告」を集約ログにまとめる。

  • それ以外(WAN2.1モデルや他モデル)では、警告を抑制せず通常どおり出す。

  • 新規でエラー表示が出た全系列(`double_blocks`/`single_blocks` の img/text ヘッド群)も抑制対象に含める。


対象ファイル

  • `ComfyUI/comfy/lora.py`


変更前(初期状態)

  • 抑制ロジックはなく、未ロードキーはすべて警告として出力。

if log_missing:
    for x in lora.keys():
        if x not in loaded_keys:
            logging.warning("lora key not loaded: {}".format(x))

問題:

  • WAN2.1 LoRAをWAN2.2に適用すると、構造差由来の未ロードキーが大量に出続け、ログがノイズ化。


変更後(完成形)

  • 変更位置: `def load_lora(...):` 内の `if log_missing:` ブロック

  • 実装の骨子:

    • LoRA側にWAN2.1特有の「キー断片」が1つでもあれば「WAN2.1 LoRA」と判定。

    • モデル側(`to_load.values()`)にWAN2.1特有断片が1つも無ければ「WAN2.2」と推定。

    • 両方が真(WAN2.1 LoRA × WAN2.2モデル)かつ、該当キーがインジケータに合致する場合のみ抑制し、件数を集約ログで出力。

    • それ以外は通常通り、個別の警告を出す。

実コード(該当部のみ抜粋):

if log_missing:
    # WAN2.1 on WAN2.2 warning suppression (expected missing keys)
    # Detect WAN2.1-style LoRA by presence of characteristic key fragments in the LoRA file
    wan21_lora_indicators = [
        # classic image-attn/text-attn specific
        "k_img.diff_b", "k_img.lora_down.weight", "k_img.lora_up.weight",
        "v_img.diff_b", "v_img.lora_down.weight", "v_img.lora_up.weight",
        "norm_k_img.diff",
        # img_emb variants (WAN2.1)
        "img_emb.proj.",
        # cross_attn variants (WAN2.1)
        ".cross_attn.k_img.", ".cross_attn.v_img.",
        # trainers: double_blocks/single_blocks heads (img/text)
        ".double_blocks.", ".single_blocks.",
        ".img_attn_proj.", ".img_attn_qkv.",
        ".img_mlp.fc1.", ".img_mlp.fc2.",
        ".img_mod.linear.",
        ".txt_attn_proj.", ".txt_attn_qkv.",
        ".txt_mlp.fc1.", ".txt_mlp.fc2.",
        ".txt_mod.linear.",
        # common single block linear heads
        ".linear1.lora_", ".linear2.lora_", ".modulation.linear."
    ]

    wan21_keys_found = [key for key in lora.keys() if any(indicator in key for indicator in wan21_lora_indicators)]
    is_wan21_lora = len(wan21_keys_found) > 0

    # Apply suppression ONLY when applying WAN2.1 LoRA onto WAN2.2 model.
    # Heuristic for model: if model (to_load values) contains WAN2.1-only keys, it's not WAN2.2.
    try:
        model_key_samples = [str(v) for v in to_load.values()]
    except Exception:
        model_key_samples = []

    model_has_wan21_like_keys = any(
        (".cross_attn.k_img." in mk) or (".cross_attn.v_img." in mk) or
        ("img_emb.proj" in mk) or ("norm_k_img" in mk)
        for mk in model_key_samples
    )
    is_wan22_model_target = not model_has_wan21_like_keys

    suppressed_count = 0
    actual_warnings = []

    for x in lora.keys():
        if x not in loaded_keys:
            # Suppress only expected WAN2.1-on-WAN2.2 missing keys
            if (is_wan21_lora and is_wan22_model_target and
                any(indicator in x for indicator in wan21_lora_indicators)):
                suppressed_count += 1
                continue
            actual_warnings.append(x)

    for x in actual_warnings:
        logging.warning("lora key not loaded: {}".format(x))

    if suppressed_count > 0:
        logging.info(
            "Suppressed {} WAN2.1 LoRA key warnings (WAN2.1 LoRA on WAN2.2 model - expected behavior)".format(
                suppressed_count
            )
        )

何を抑制対象にしたか(部分一致インジケータ)

  • k_img/v_img 系:

    • "k_img.diff_b", "k_img.lora_down.weight", "k_img.lora_up.weight"

    • "v_img.diff_b", "v_img.lora_down.weight", "v_img.lora_up.weight"

    • "norm_k_img.diff"

  • img_emb 系:

    • "img_emb.proj."

  • cross_attn 系:

    • ".cross_attn.k_img.", ".cross_attn.v_img."

  • 新たに提示された系列も網羅:

    • ".double_blocks.", ".single_blocks."

    • ".img_attn_proj.", ".img_attn_qkv."

    • ".img_mlp.fc1.", ".img_mlp.fc2."

    • ".img_mod.linear."

    • ".txt_attn_proj.", ".txt_attn_qkv."

    • ".txt_mlp.fc1.", ".txt_mlp.fc2."

    • ".txt_mod.linear."

    • ".linear1.lora_", ".linear2.lora_", ".modulation.linear."

注意:

  • 末尾の `.weight` までの完全一致にしない(トレーナー差により末端が揺れるため)。部分一致で十分かつ安全。


再現手順

  • 対象ファイル: `ComfyUI/comfy/lora.py`

  • `load_lora` の `if log_missing:` ブロックを上記の完成形に差し替える

  • ComfyUIを再起動(または該当モジュール再ロード)

  • ログレベルを info 以上に設定(集約ログは info)


動作検証の観点

  • WAN2.2 × WAN2.1 LoRA:

    • 個別の未ロード警告は抑制され、末尾に集約ログのみ表示

    • 例: `Suppressed 290 WAN2.1 LoRA key warnings (WAN2.1 LoRA on WAN2.2 model - expected behavior)`

  • WAN2.2 × WAN2.2 LoRA:

    • 抑制は発動しない(通常は未ロード警告自体が出ない想定)

  • WAN2.1 × WAN2.1 LoRA(または他モデル):

    • 抑制は発動しない(個別警告が出る)


トラブルシューティング

  • 抑制されないキーがあった場合:

    • そのキー名の一部を `wan21_lora_indicators` に1行追加(部分一致でOK)

  • 抑制が効きすぎる場合(本来出すべき環境で消える):

    • モデル側判定(`model_has_wan21_like_keys`)の条件をより厳格化

  • ログが出ない:

    • ログレベルを info 以上に設定


変更のインパクト

  • WAN2.2 × WAN2.1適用時のログノイズを大幅に削減

  • WAN2.2以外の組み合わせでは従来通りの警告表示を維持(安全側)


このノートの内容で、第三者が後から読んでも同じ実装・同じ挙動を再現できます。必要になったら `wan21_lora_indicators` に断片を1行足すだけで拡張可能です。

About WAN2.1 LoRA

WAn2.1用LoRAの適用に関しては一概に総括できませんが、以下はHjgh側でstrength3.0、Low側で1.5で想定通りの動作をした例です。360度回転モーションLoRAを使用しています。

WAN2.2はプロンプト再現能力が非常に高く、実はこの程度の事はプロンプトだけでやれてしまいますが。

Fixed GGUF Loader for WAN2.2

この先はともかく、少なくとも現時点で肝心のGGUFローダーがWAN2.2に対応していません。

故に改造しています。以下、特に難しくありません。厳密に言えば、「GGUFローダーの記述を改造する事により、MultiGPUのGGUFローダーがWAN2.2を認識する」という仕組みになっています。

## 修正箇所

**ファイル**: `ComfyUI/custom_nodes/ComfyUI-GGUF/loader.py`

### 修正前のコード

IMG_ARCH_LIST = {"flux", "sd1", "sdxl", "sd3", "aura", "hidream", "cosmos", "ltxv", "hyvid", "wan"}

### 修正後のコード

IMG_ARCH_LIST = {"flux", "sd1", "sdxl", "sd3", "aura", "hidream", "cosmos", "ltxv", "hyvid", "wan", "wan2.2"}

```

Distorch Memory Manager

About a node

30GBクラスのベースモデル2個に対してdistorch処理をかける工程は、何か影響が出る予感がしていましたが、案の定結果RTX4070 12GBクラスではx4 UpscaleがOOMの為完走しないという問題を引き起こしました。

但し、私は既に独自の改造により解決しています。まずは下図のように、Distorchによるメモリ使用をアンロードする為のオリジナルノードを作成しました。

independently Distorch Memory Cleaner

ノードは、以下記事で公開しています。ダウンロードして解凍し、custom_nodesフォルダに置くだけで動作します。

custom_nodes\ComfyUI-MultiGPU_init_.py

難易度が高くなるので推奨はしませんが、同機能を以下の様にMultiGPUに統合する事も可能です。但し、この方法はファイル構造やコードの内容を理解できるユーザーでないと、使い続ける事が難しいでしょう。理由は、上記事にて記述しています。 

DisTorch Memory Managerの改造詳細をコード付きで解説します。

## 📋 DisTorch Memory Manager 改造総括

### 1. **DisTorchMemoryCleaner** - 基本的なメモリクリア

class DisTorchMemoryCleaner:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {
            "image": ("IMAGE",),
        }}
    
    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "clean_memory"
    CATEGORY = "DisTorch"

    def clean_memory(self, image):
        import torch
        import gc
        
        # GPUメモリのクリア
        if torch.cuda.is_available():
            torch.cuda.empty_cache()
            torch.cuda.synchronize()
        
        # Pythonのガベージコレクション
        gc.collect()
        
        # DisTorchの仮想メモリを解放
        try:
            import comfy.model_management
            # 仮想メモリの割り当てをリセット
            if hasattr(comfy.model_management, 'free_memory'):
                comfy.model_management.free_memory(0, 'cuda:0')
                comfy.model_management.free_memory(0, 'cpu')
        except:
            pass
        
        print("DisTorch memory cleaned")
        return (image,)

**機能**:

- GPUメモリのクリア(`torch.cuda.empty_cache()`)

- Pythonガベージコレクション(`gc.collect()`)

- DisTorch仮想メモリのリセット

- シンプルで安全なメモリ管理

### 2. **DisTorchMemoryManager** - 包括的なメモリ管理(UI破損対策済み)

class DisTorchMemoryManager:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {
            "image": ("IMAGE",),
            "clean_gpu": ("BOOLEAN", {"default": True}),
            "clean_cpu": ("BOOLEAN", {"default": False, "tooltip": "CPU memory cleanup (use with caution)"}),
            "force_gc": ("BOOLEAN", {"default": True}),
            "reset_virtual_memory": ("BOOLEAN", {"default": True}),
            "restore_original_functions": ("BOOLEAN", {"default": False, "tooltip": "Restore original model_management functions"}),
        }}
    
    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "manage_memory"
    CATEGORY = "DisTorch"

    def manage_memory(self, image, clean_gpu, clean_cpu, force_gc, reset_virtual_memory, restore_original_functions):
        import torch
        import gc
        import psutil
        import pydantic
        
        print("=== DisTorch Memory Management ===")
        
        # メモリ使用量の表示(前)
        if torch.cuda.is_available():
            gpu_memory_before = torch.cuda.memory_allocated() / 1024**3
            print(f"GPU Memory before: {gpu_memory_before:.2f} GB")
        
        cpu_memory_before = psutil.virtual_memory().used / 1024**3
        print(f"CPU Memory before: {cpu_memory_before:.2f} GB")
        
        # GPUメモリのクリア
        if clean_gpu and torch.cuda.is_available():
            try:
                torch.cuda.empty_cache()
                torch.cuda.synchronize()
                print("GPU cache cleared")
            except Exception as e:
                print(f"GPU cache clear failed: {e}")
        
        # CPUメモリのクリア(UI破損対策済み)
        if clean_cpu:
            # 基本的なガベージコレクションのみ実行
            # オブジェクトの詳細検査は行わない(UIに影響する可能性があるため)
            try:
                # 明示的にガベージコレクションを実行
                collected = gc.collect()
                print(f"CPU memory cleanup: {collected} objects collected")
            except Exception as e:
                print(f"CPU memory cleanup failed: {e}")
        
        # ガベージコレクション
        if force_gc:
            try:
                collected = gc.collect()
                print(f"Garbage collected: {collected} objects")
            except Exception as e:
                print(f"Garbage collection failed: {e}")
        
        # DisTorchの仮想メモリをリセット
        if reset_virtual_memory:
            try:
                import comfy.model_management
                if hasattr(comfy.model_management, 'free_memory'):
                    comfy.model_management.free_memory(0, 'cuda:0')
                    comfy.model_management.free_memory(0, 'cpu')
                print("Virtual memory reset")
            except Exception as e:
                print(f"Virtual memory reset failed: {e}")
        
        # メモリ使用量の表示(後)
        if torch.cuda.is_available():
            try:
                gpu_memory_after = torch.cuda.memory_allocated() / 1024**3
                gpu_freed = gpu_memory_before - gpu_memory_after
                print(f"GPU Memory after: {gpu_memory_after:.2f} GB (freed: {gpu_freed:.2f} GB)")
            except Exception as e:
                print(f"GPU memory measurement failed: {e}")
        
        try:
            cpu_memory_after = psutil.virtual_memory().used / 1024**3
            cpu_freed = cpu_memory_before - cpu_memory_after
            print(f"CPU Memory after: {cpu_memory_after:.2f} GB (freed: {cpu_freed:.2f} GB)")
        except Exception as e:
            print(f"CPU memory measurement failed: {e}")
        
        # 元の関数を復元(オプション)
        if restore_original_functions:
            try:
                mm.get_torch_device = original_get_torch_device
                mm.text_encoder_device = original_text_encoder_device
                print("Original model_management functions restored")
            except Exception as e:
                print(f"Failed to restore original functions: {e}")
        
        print("=== Memory Management Complete ===")
        
        return (image,)

**主要な改善点**:

1. **UI破損対策**:

   # CPUメモリクリアを安全に実行
   if clean_cpu:
       # オブジェクトの詳細検査は行わない
       try:
           collected = gc.collect()
           print(f"CPU memory cleanup: {collected} objects collected")
       except Exception as e:
           print(f"CPU memory cleanup failed: {e}")

  2. **詳細なメモリ監視**:

   # メモリ使用量の前後比較
   gpu_memory_before = torch.cuda.memory_allocated() / 1024**3
   cpu_memory_before = psutil.virtual_memory().used / 1024**3
   
   # 処理後
   gpu_freed = gpu_memory_before - gpu_memory_after
   cpu_freed = cpu_memory_before - cpu_memory_after

   3. **エラーハンドリング**:

   try:
       torch.cuda.empty_cache()
       torch.cuda.synchronize()
       print("GPU cache cleared")
   except Exception as e:
       print(f"GPU cache clear failed: {e}")

### 3. **DisTorchSafeMemoryManager** - 安全なメモリ管理(推奨)

class DisTorchSafeMemoryManager:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {
            "image": ("IMAGE",),
            "clean_gpu": ("BOOLEAN", {"default": True}),
            "force_gc": ("BOOLEAN", {"default": True}),
            "reset_virtual_memory": ("BOOLEAN", {"default": True}),
        }}
    
    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "safe_manage_memory"
    CATEGORY = "DisTorch"

    def safe_manage_memory(self, image, clean_gpu, force_gc, reset_virtual_memory):
        import torch
        import gc
        
        print("=== DisTorch Safe Memory Management ===")
        
        # メモリ使用量の表示
        if torch.cuda.is_available():
            gpu_memory_before = torch.cuda.memory_allocated() / 1024**3
            print(f"GPU Memory before: {gpu_memory_before:.2f} GB")
        
        # GPUメモリのクリア(UIに影響しない)
        if clean_gpu and torch.cuda.is_available():
            try:
                torch.cuda.empty_cache()
                torch.cuda.synchronize()
                print("GPU cache cleared")
            except Exception as e:
                print(f"GPU cache clear failed: {e}")
        
        # ガベージコレクション(UIに影響しない)
        if force_gc:
            try:
                collected = gc.collect()
                print(f"Garbage collected: {collected} objects")
            except Exception as e:
                print(f"Garbage collection failed: {e}")
        
        # DisTorchの仮想メモリをリセット
        if reset_virtual_memory:
            try:
                import comfy.model_management
                if hasattr(comfy.model_management, 'free_memory'):
                    comfy.model_management.free_memory(0, 'cuda:0')
                    comfy.model_management.free_memory(0, 'cpu')
                print("Virtual memory reset")
            except Exception as e:
                print(f"Virtual memory reset failed: {e}")
        
        # メモリ使用量の表示(後)
        if torch.cuda.is_available():
            try:
                gpu_memory_after = torch.cuda.memory_allocated() / 1024**3
                gpu_freed = gpu_memory_before - gpu_memory_after
                print(f"GPU Memory after: {gpu_memory_after:.2f} GB (freed: {gpu_freed:.2f} GB)")
            except Exception as e:
                print(f"GPU memory measurement failed: {e}")
        
        print("=== Safe Memory Management Complete ===")
        
        return (image,)

**特徴**:

- **CPUメモリクリア機能を完全に除外**(UI破損を防ぐ)

- **GPUメモリとガベージコレクションのみ**実行

- **最も安全で安定した**メモリ管理

## �� **改造の核心ポイント**

### 1. **UI破損問題の解決**

# 問題のあったコード(削除済み)
# for obj in gc.get_objects():
#     if isinstance(obj, pydantic.BaseModel):
#         # この部分がUI破損の原因

# 解決策
if clean_cpu:
    # 基本的なガベージコレクションのみ実行
    collected = gc.collect()
    print(f"CPU memory cleanup: {collected} objects collected")

### 2. **メモリ使用量の可視化**

# メモリ解放量の詳細表示
gpu_freed = gpu_memory_before - gpu_memory_after
cpu_freed = cpu_memory_before - cpu_memory_after
print(f"GPU Memory freed: {gpu_freed:.2f} GB")
print(f"CPU Memory freed: {cpu_freed:.2f} GB")

### 3. **エラーハンドリングの強化**

try:
    torch.cuda.empty_cache()
    torch.cuda.synchronize()
    print("GPU cache cleared")
except Exception as e:
    print(f"GPU cache clear failed: {e}")

## �� **使用方法**

# DisTorchSafeMemoryManager(最も安全)
clean_gpu = True
force_gc = True
reset_virtual_memory = True

# DisTorchMemoryManager(上級者向け)
clean_gpu = True
clean_cpu = False  # UI破損を防ぐためFalse推奨
force_gc = True
reset_virtual_memory = True

### **効果**:

- **GPUメモリ**: 最大数GBの解放

- **CPUメモリ**: 数百MB〜数GBの解放

- **安定性**: UI破損なし

- **パフォーマンス**: アップスケール・ビデオ処理の安定化

これらの改造により、ComfyUIでのメモリ管理が大幅に改善され、アップスケールやビデオ処理でのメモリエラーが大幅に減少します。

Paging file

このノードを使用しても尚、upscalingでOOM(この場合、VRAMではなくシステムメモリ不足で落ちる)エラーを起こす場合、ページングファイルのサイズを拡張して対応してください。

高速化ノード組み合わせと、テンソル不一致エラーについて

初期状態での組み合わせは、Skip Layer+Torch Compileですが、この組み合わせはどうも、720×720生成時にテンソル不一致エラーの原因になるようです。稀に640×640でも発生しますが、条件がどうも不明です。

Skip Layerをバイパスして、代わりにApply First Block Cacheを使用すると解決する場合が多いです。何れにせよ、組み合わせを選択できるようにしておきました 。

Torch.compileは無いと、特に640×640以上で相当生成速度が落ちるので、なるべく使用したい処です。

今日のBGM

鞘師里保 - Super Red (Music Video)


いいなと思ったら応援しよう!