SYSTEM NOTICE

Auto translation by AI. Be sure, accuracy, nuances and authorial intent may not be fully reflected.
見出し画像

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


On October 8, 2025, I updated Ver II 4steps, which applies the lightning LoRA for WAN2.2.

It is quite good. The concept above was a form that was quite difficult to achieve with WAN2.1.

About WAN2.2

i2v with WAN2.1 lightx2v LoRA 6steps

i2v II with WAN2.2 lightning LoRA 4steps

It is the usual setup. Even when using the full-size FP16 GGUF, the VRAM consumption is kept under 12GB thanks to Distorch, a modified MultiGPU node... It is becoming my standard every time a new video generation model is released.

It is faster than I thought. Distorch is familiar, but surprisingly, the single-task performance is faster than WAN2.1.

Also, as shown below, it is known that reducing the number of steps using lightx2v is effective. However, currently, using the LoRA for WAN2.1 outputs a large number of errors, so that part is not very smart. This error itself is unavoidable, but it is possible to "handle" the display itself, which I will describe later.

Regarding the Distorch modification, please refer to the following.

At 480x480, the processing looks like this. It seems that VRAM consumption stays under 12GB up to around 720x720 with a length of 121. The JSON above is set to 640x640 with a length of 121.

fp16 GGUF

I have released the unquantized full-size GGUF below.

Ollama-Generate V2

Although it makes the operation heavier, I use Ollama to provide an automatic prompt generation function. It also supports vision-based LLMs, but honestly, the operational accuracy is not good. A standard text-generation LLM is safer.

In return, prompt input supports Japanese. It is designed to perform automatic translation with Ollama Generate V2.

Fixed lora.py

\ComfyUI\comfy\lora.py"

It does not affect operation, but applying the lightx2v LoRA originally intended for WAN2.1 to WAN2.2 results in a large number of

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

type errors. It is not a pleasant feeling, so I have applied a fix to convert this as follows, but only "when applying a WAN2.1 LoRA to WAN2.2," and I have released it above.

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

Explanation

Implementation note for aggregating and suppressing unloaded key warnings only when applying WAN2.1 LoRA to WAN2.2 models (ComfyUI `lora.py`)

Objective

  • Aggregate the large volume of "expected unloaded key warnings" into a summary log only when applying WAN2.1 LoRA to WAN2.2.

  • In all other cases (WAN2.1 models or other models), output warnings as usual without suppression.

  • Include all newly error-prone series (`double_blocks`/`single_blocks` img/text heads) in the suppression scope.


Target file

  • `ComfyUI/comfy/lora.py`


Before change (initial state)

  • There is no suppression logic; all unloaded keys are output as warnings.

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

Problem:

  • When applying WAN2.1 LoRA to WAN2.2, a large number of unloaded keys derived from structural differences continue to appear, turning the logs into noise.


After change (final form)

  • Change location: `if log_missing:` block within `def load_lora(...):`

  • Core of implementation:

    • If the LoRA side contains even one "key fragment" specific to WAN2.1, it is identified as a "WAN2.1 LoRA".

    • If the model side (`to_load.values()`) contains no WAN2.1-specific fragments, it is presumed to be "WAN2.2".

    • Only when both are true (WAN2.1 LoRA × WAN2.2 model) and the corresponding key matches the indicator, suppress it and output the count in the summary log.

    • Otherwise, output individual warnings as usual.

Actual code (relevant section only):

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
            )
        )

What was targeted for suppression (partial match indicator)

  • k_img/v_img series:

    • "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 series:

    • "img_emb.proj."

  • cross_attn series:

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

  • Also covering the newly presented sequences:

    • ".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."

Note:

  • Do not use an exact match up to the trailing `.weight` (as the ends may vary due to differences in trainers). Partial matching is sufficient and safe.


Reproduction steps

  • Target file: `ComfyUI/comfy/lora.py`

  • Replace the `if log_missing:` block in `load_lora` with the completed version above.

  • Restart ComfyUI (or reload the relevant module).

  • Set the log level to info or higher (aggregated logs are info).


Points for operational verification

  • WAN2.2 × WAN2.1 LoRA:

    • Individual un-loaded warnings are suppressed, and only an aggregated log is displayed at the end

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

  • WAN2.2 × WAN2.2 LoRA:

    • Suppression is not triggered (normally, un-loaded warnings themselves are not expected to appear)

  • WAN2.1 × WAN2.1 LoRA (or other models):

    • Suppression is not triggered (individual warnings appear)


Troubleshooting

  • If there are keys that are not suppressed:

    • Add a part of the key name to `wan21_lora_indicators` on a new line (partial match is fine)

  • If suppression is too aggressive (it disappears in environments where it should appear):

    • Make the conditions for model-side determination (`model_has_wan21_like_keys`) more strict

  • No logs appearing:

    • Set log level to info or higher


Impact of changes

  • Significantly reduced log noise when applying WAN2.2 × WAN2.1

  • Maintained conventional warning displays for combinations other than WAN2.2 (for safety)


With the contents of this notebook, a third party can reproduce the same implementation and behavior later. If needed, it can be extended by simply adding one line of fragment to `wan21_lora_indicators`.

About WAN2.1 LoRA

While the application of LoRA for WAN2.1 cannot be summarized in a single statement, the following is an example where it worked as expected with strength 3.0 on the High side and 1.5 on the Low side. I am using a 360-degree rotation motion LoRA.

WAN2.2 has extremely high prompt reproduction capability, so actually, this level of task can be done with just prompts.

Fixed GGUF Loader for WAN2.2

Regardless of what happens later, at least at the current moment, the essential GGUF loader does not support WAN2.2.

Therefore, I have modified it. The following is not particularly difficult. Strictly speaking, it is structured such that “by modifying the GGUF loader description, the MultiGPU GGUF loader recognizes WAN2.2”.

## Modification points

**File**: `ComfyUI/custom_nodes/ComfyUI-GGUF/loader.py`

### Code before modification

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

### Code after modification

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

```

Distorch Memory Manager

About a node

I had a feeling that the process of applying distorch processing to two 30GB-class base models would have some impact, and as expected, it caused an issue where x4 Upscale would not complete due to OOM on RTX4070 12GB-class hardware.

However, I have already solved this through my own modifications. First, as shown in the figure below, I created an original node to unload memory usage by Distorch.

independently Distorch Memory Cleaner

The nodes are published in the article below. Simply download, unzip, and place them in the custom_nodes folder to use them.

custom_nodes\ComfyUI-MultiGPU_init_.py

I do not recommend this as it increases the difficulty, but it is possible to integrate the same functionality into MultiGPU as shown below. However, this method will be difficult for users to maintain unless they understand the file structure and the content of the code. The reasons are described in the article above.

I will explain the details of the DisTorch Memory Manager modifications, including the code.

## 📋 DisTorch Memory Manager Modification Summary

### 1. **DisTorchMemoryCleaner** - Basic memory clearing

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,)

**Functionality**:

- GPU memory clearing (`torch.cuda.empty_cache()`)

- Python garbage collection (`gc.collect()`)

- Resetting DisTorch virtual memory

- Simple and safe memory management

### 2. **DisTorchMemoryManager** - Comprehensive memory management (with UI corruption prevention)

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,)

**Key improvements**:

1. **UI corruption prevention**:

   # 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. **Detailed memory monitoring**:

   # メモリ使用量の前後比較
   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. **Error handling**:

   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** - Safe memory management (recommended)

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,)

**Features**:

- **Completely excludes CPU memory clearing functionality** (prevents UI corruption)

- **Execute GPU memory and garbage collection only**

- **The safest and most stable** memory management

## 🛠 **Core points of the modification**

### 1. **Resolution of UI corruption issues**

# 問題のあったコード(削除済み)
# 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. **Visualization of memory usage**

# メモリ解放量の詳細表示
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. **Enhanced error handling**

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

## 🚀 **How to use**

# 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

### **Effects**:

- **GPU memory**: Frees up several GBs

- **CPU memory**: Frees up several hundred MBs to several GBs

- **Stability**: No UI corruption

- **Performance**: Stabilizes upscaling and video processing

These modifications significantly improve memory management in ComfyUI, greatly reducing memory errors during upscaling and video processing.

Paging file

If you still encounter OOM errors (in this case, crashing due to system memory shortage rather than VRAM) during upscaling even after using this node, please expand your paging file size to address it.

Regarding acceleration node combinations and tensor mismatch errors

The default combination is Skip Layer + Torch Compile, but this combination seems to be the cause of tensor mismatch errors when generating at 720x720. It rarely occurs at 640x640 as well, but the conditions are unclear.

Bypassing Skip Layer and using Apply First Block Cache instead often resolves the issue. In any case, I have made it possible to select the combinations.

Without Torch.compile, generation speed drops significantly, especially at 640x640 or higher, so I would like to use it as much as possible.

Today's BGM

Riho Sayashi - Super Red (Music Video)


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