Instructions to use charles2530/Wan2.2-NVFP4-Sparse with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use charles2530/Wan2.2-NVFP4-Sparse with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("charles2530/Wan2.2-NVFP4-Sparse", torch_dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Add files using upload-large-folder tool
Browse files- README.md +136 -0
- Wan2.2-I2V-A14B_NVFP4_Sparse_high.safetensors +3 -0
- Wan2.2-I2V-A14B_NVFP4_Sparse_high_comfy.safetensors +3 -0
- Wan2.2-I2V-A14B_NVFP4_Sparse_low.safetensors +3 -0
- Wan2.2-I2V-A14B_NVFP4_Sparse_low_comfy.safetensors +3 -0
- Wan2.2-T2V-A14B_NVFP4_Sparse_high.safetensors +3 -0
- Wan2.2-T2V-A14B_NVFP4_Sparse_low.safetensors +3 -0
- Wan2.2-T2V-A14B_NVFP4_Sparse_low_comfy.safetensors +1 -1
- __pycache__/convert_lightx2v_nvfp4_to_comfy.cpython-312.pyc +0 -0
- __pycache__/verify_comfy_nvfp4.cpython-312.pyc +0 -0
- __pycache__/verify_comfy_source_compat.cpython-312.pyc +0 -0
- analysis.md +138 -0
- convert_lightx2v_nvfp4_to_comfy.py +172 -0
- verify_comfy_nvfp4.py +167 -0
- verify_comfy_source_compat.py +67 -0
README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model:
|
| 4 |
+
- Wan-AI/Wan2.2-T2V-A14B
|
| 5 |
+
library_name: diffusers
|
| 6 |
+
tags:
|
| 7 |
+
- video_generation
|
| 8 |
+
- NVFP4
|
| 9 |
+
- Sparse_Attention
|
| 10 |
+
- Wan
|
| 11 |
+
---
|
| 12 |
+
# 🎬 Wan2.2-NVFP4-Sparse
|
| 13 |
+
|
| 14 |
+
> **An extremely efficient Wan 2.2 14B variant: NVFP4 Quantization-Aware Step Distillation with Sparse Attention for Blackwell Architecture**
|
| 15 |
+
|
| 16 |
+
[](https://github.com/ModelTC/LightX2V)
|
| 17 |
+
[](https://huggingface.co/lightx2v/)
|
| 18 |
+
|
| 19 |
+
## 📋 Table of Contents
|
| 20 |
+
|
| 21 |
+
- [✨ Features](#-features)
|
| 22 |
+
- [🚀 Quick Start](#-quick-start)
|
| 23 |
+
- [🎬 Generation Results](#-generation-results)
|
| 24 |
+
- [⚡ Performance Comparison](#-performance-comparison)
|
| 25 |
+
- [⚠️ Notes](#️-notes)
|
| 26 |
+
- [🤝 Community](#-community)
|
| 27 |
+
|
| 28 |
+
## ✨ Features
|
| 29 |
+
|
| 30 |
+
- **⚡ 4-Step Inference**: Two high-noise expert steps followed by two low-noise expert steps, enabling extremely fast Wan2.2 MoE generation on a single Blackwell GPU.
|
| 31 |
+
- **🎯 NVFP4 Quantization**: Quantization-aware step distillation reduces memory traffic and compute cost while targeting Blackwell architecture.
|
| 32 |
+
- **🧩 Sparse Attention**: Accelerates the costly O(n²) self-attention workload with sparse attention, reducing end-to-end latency for high-resolution video generation.
|
| 33 |
+
- **🔧 LightX2V Integration**: Recommended runtime stack for stable deployment and best performance.
|
| 34 |
+
- **🚀 High-Quality Generation**: Preserves the visual quality of Wan2.2-T2V-14B while dramatically improving inference speed.
|
| 35 |
+
|
| 36 |
+
## 🚀 Quick Start
|
| 37 |
+
|
| 38 |
+
We strongly recommend using the official LightX2V Docker image for the cleanest environment and best reproducibility.
|
| 39 |
+
|
| 40 |
+
### Option A: Docker Recommended
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
# 1. Pull LightX2V Docker image
|
| 44 |
+
docker pull lightx2v/lightx2v:26052801-cu130-5090
|
| 45 |
+
|
| 46 |
+
# 2. Run inference
|
| 47 |
+
bash scripts/wan22/distill/run_wan22_moe_t2v_extreme.sh
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Option B: Manual Installation
|
| 51 |
+
|
| 52 |
+
If Docker is not available, install the environment manually:
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# 1. Install LightX2V
|
| 56 |
+
git clone https://github.com/ModelTC/LightX2V.git
|
| 57 |
+
cd LightX2V
|
| 58 |
+
uv pip install -v .
|
| 59 |
+
|
| 60 |
+
# 2. Install NVFP4 Kernel
|
| 61 |
+
pip install scikit_build_core uv
|
| 62 |
+
git clone https://github.com/NVIDIA/cutlass.git
|
| 63 |
+
cd lightx2v_kernel
|
| 64 |
+
|
| 65 |
+
MAX_JOBS=$(nproc) CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) \
|
| 66 |
+
uv build --wheel \
|
| 67 |
+
-Cbuild-dir=build . \
|
| 68 |
+
-Ccmake.define.CUTLASS_PATH=/path/to/cutlass \
|
| 69 |
+
--verbose --color=always --no-build-isolation
|
| 70 |
+
|
| 71 |
+
pip install dist/*whl --force-reinstall --no-deps
|
| 72 |
+
|
| 73 |
+
# 3. Run inference
|
| 74 |
+
bash scripts/wan22/distill/run_wan22_moe_t2v_extreme.sh
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
Script: [run_wan22_moe_t2v_extreme.sh](https://github.com/ModelTC/LightX2V/blob/main/scripts/wan22/distill/run_wan22_moe_t2v_extreme.sh)
|
| 78 |
+
|
| 79 |
+
## 🎬 Generation Results
|
| 80 |
+
|
| 81 |
+
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; margin: 16px 0;">
|
| 82 |
+
<p style="font-style: italic; color: #475569; margin: 0; padding: 12px; background: white; border-radius: 6px; border-left: 4px solid #3b82f6;">
|
| 83 |
+
"Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage"
|
| 84 |
+
</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
| Resolution | Wan2.2-T2V-14B | Wan2.2-NVFP4-Sparse |
|
| 89 |
+
| --- | --- | --- |
|
| 90 |
+
| 480p | <video controls style="width: 260px; height: 180px; border-radius: 6px; object-fit: cover;" src="https://cdn-uploads.huggingface.co/production/uploads/658e760cccbc1e2cc78b4258/WTHhrzx7XR4S1Ys_6Kzx4.mp4"></video> | <video controls style="width: 260px; height: 180px; border-radius: 6px; object-fit: cover;" src="https://cdn-uploads.huggingface.co/production/uploads/658e760cccbc1e2cc78b4258/zorpw7gm9At0J2kCmvkDr.mp4"></video> |
|
| 91 |
+
| 720p | <video controls style="width: 260px; height: 180px; border-radius: 6px; object-fit: cover;" src="https://cdn-uploads.huggingface.co/production/uploads/658e760cccbc1e2cc78b4258/vkiyKj7CJA-r0yTz7TEum.mp4"></video> | <video controls style="width: 260px; height: 180px; border-radius: 6px; object-fit: cover;" src="https://cdn-uploads.huggingface.co/production/uploads/658e760cccbc1e2cc78b4258/TuECbzvW5jI9NHG6GLvIR.mp4"></video> |
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
## ⚡ Performance Comparison
|
| 95 |
+
|
| 96 |
+
**Test Environment**: RTX 5090 Single GPU | LightX2V Framework | End-to-End Latency
|
| 97 |
+
|
| 98 |
+
| Resolution | Wan2.2-T2V-14B | Wan2.2-NVFP4-Sparse | Speedup |
|
| 99 |
+
| --- | ---: | ---: | ---: |
|
| 100 |
+
| 480p | 734s | 14.15s | 51.9x |
|
| 101 |
+
| 720p | 2668s | 45s | 59.3x |
|
| 102 |
+
|
| 103 |
+
## ⚠️ Notes
|
| 104 |
+
|
| 105 |
+
### System Requirements
|
| 106 |
+
|
| 107 |
+
- **Required Hardware**: NVIDIA RTX 50-series GPUs or other Blackwell architecture GPUs.
|
| 108 |
+
- **Recommended Runtime**: `lightx2v/lightx2v:26052801-cu130-5090`.
|
| 109 |
+
|
| 110 |
+
### Dependencies
|
| 111 |
+
|
| 112 |
+
- Prepare Wan2.2 T5 / VAE components following the standard LightX2V Wan2.2 model structure.
|
| 113 |
+
- Use Blackwell + NVFP4 kernels for optimal speed and memory efficiency.
|
| 114 |
+
|
| 115 |
+
### Performance Tips
|
| 116 |
+
|
| 117 |
+
- Use the provided extreme inference script for the 4-step high-noise / low-noise expert schedule.
|
| 118 |
+
- Sparse attention is most beneficial at higher resolutions where self-attention dominates latency.
|
| 119 |
+
- Enable CPU offload only when GPU memory is limited, since offload can reduce throughput.
|
| 120 |
+
|
| 121 |
+
## 🤝 Community
|
| 122 |
+
|
| 123 |
+
- **🐛 Issues**: [GitHub Issues](https://github.com/ModelTC/LightX2V/issues)
|
| 124 |
+
- **🤗 Models**: [HuggingFace Hub](https://huggingface.co/lightx2v/)
|
| 125 |
+
- **📖 Documentation**: [LightX2V Docs](https://github.com/ModelTC/LightX2V)
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
<div align="center">
|
| 130 |
+
|
| 131 |
+
**If you find this project helpful, please give us a ⭐ on [GitHub](https://github.com/ModelTC/LightX2V)**
|
| 132 |
+
|
| 133 |
+
For questions or issues, please open an issue on [LightX2V](https://github.com/ModelTC/LightX2V/issues) or contact lvchengtao0319@gmail.com.
|
| 134 |
+
|
| 135 |
+
</div>
|
| 136 |
+
|
Wan2.2-I2V-A14B_NVFP4_Sparse_high.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c77b92616d678fa8373462973a3d2e3ceefd2fbdb3ccf33454e74f4c79fcf07a
|
| 3 |
+
size 8379833592
|
Wan2.2-I2V-A14B_NVFP4_Sparse_high_comfy.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:998934b07f124111b83c3bfcdf9efc2ff67ff531879806e54a1453c14066a5db
|
| 3 |
+
size 8379918824
|
Wan2.2-I2V-A14B_NVFP4_Sparse_low.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d76b53286dd9b68f5d956941037c0a6fd5a1fe0fd05eb97f8bf8abf9915da562
|
| 3 |
+
size 8379833592
|
Wan2.2-I2V-A14B_NVFP4_Sparse_low_comfy.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a26076d43590941a6526ed52d2b682a9340c81f650ada0751a90e5bdcde5efb7
|
| 3 |
+
size 8379918824
|
Wan2.2-T2V-A14B_NVFP4_Sparse_high.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1da94b1604f4bb181d1319f311245e39e33e499b3a0a5bf2fafb117b08750ac7
|
| 3 |
+
size 8379014392
|
Wan2.2-T2V-A14B_NVFP4_Sparse_low.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9feda8a7f4791c6e4f0dd8ab383fa67d429441dfb006b97a1e3b5400e74bc232
|
| 3 |
+
size 8379014392
|
Wan2.2-T2V-A14B_NVFP4_Sparse_low_comfy.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 8379099624
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cdccd3005e0050a700a9dec435ab3ef37923294ba03b3132b53f94460f377f6d
|
| 3 |
size 8379099624
|
__pycache__/convert_lightx2v_nvfp4_to_comfy.cpython-312.pyc
ADDED
|
Binary file (8.75 kB). View file
|
|
|
__pycache__/verify_comfy_nvfp4.cpython-312.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
__pycache__/verify_comfy_source_compat.cpython-312.pyc
ADDED
|
Binary file (4.33 kB). View file
|
|
|
analysis.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Wan2.2 NVFP4 Sparse to ComfyUI Conversion Analysis
|
| 2 |
+
|
| 3 |
+
## Sources checked
|
| 4 |
+
|
| 5 |
+
- Kijai Hugging Face repo: https://huggingface.co/Kijai/WanVideo_comfy_nvfp4
|
| 6 |
+
- ComfyUI Wan2.2 workflow docs: https://docs.comfy.org/tutorials/video/wan/wan2_2
|
| 7 |
+
- ComfyUI Wan2.2 examples: https://comfyanonymous.github.io/ComfyUI_examples/wan22/
|
| 8 |
+
- ComfyUI mixed precision loader reference:
|
| 9 |
+
https://huggingface.co/mhnakif/comfy/blob/main/comfy/ops.py
|
| 10 |
+
- ComfyUI quant op reference:
|
| 11 |
+
https://huggingface.co/mhnakif/comfy/blob/main/comfy/quant_ops.py
|
| 12 |
+
- Comfy Kitchen hardware/backend reference:
|
| 13 |
+
https://github.com/Comfy-Org/comfy-kitchen
|
| 14 |
+
- Local ComfyUI source checkout used for verification:
|
| 15 |
+
`Comfy-Org/ComfyUI` commit
|
| 16 |
+
`5aa71b9bc28809a16596bb9fa3d0a6300d8e3f0e`
|
| 17 |
+
|
| 18 |
+
## Script provenance
|
| 19 |
+
|
| 20 |
+
`convert_lightx2v_nvfp4_to_comfy.py` is a local conversion script written for
|
| 21 |
+
this directory. It is not copied from one upstream script. The implementation is
|
| 22 |
+
derived from these upstream pages and ComfyUI source conventions:
|
| 23 |
+
|
| 24 |
+
- Kijai model page:
|
| 25 |
+
https://huggingface.co/Kijai/WanVideo_comfy_nvfp4
|
| 26 |
+
- This page gives the actual LightX2V NVFP4 to Comfy NVFP4 conversion rule:
|
| 27 |
+
nibble-swap packed U8 weights, keep `weight_scale`, set
|
| 28 |
+
`weight_scale_2 = alpha * input_global_scale`, and set
|
| 29 |
+
`input_scale = 1 / input_global_scale`.
|
| 30 |
+
- ComfyUI quantized loader:
|
| 31 |
+
https://github.com/Comfy-Org/ComfyUI/blob/5aa71b9bc28809a16596bb9fa3d0a6300d8e3f0e/comfy/ops.py#L1058-L1091
|
| 32 |
+
- This loader reads `{layer}.comfy_quant`, branches on `format == "nvfp4"`,
|
| 33 |
+
then requires `{layer}.weight_scale_2` and `{layer}.weight_scale`.
|
| 34 |
+
- ComfyUI quant algorithm registry:
|
| 35 |
+
https://github.com/Comfy-Org/ComfyUI/blob/5aa71b9bc28809a16596bb9fa3d0a6300d8e3f0e/comfy/quant_ops.py#L190-L205
|
| 36 |
+
- This defines the `nvfp4` storage dtype as `torch.uint8` and the parameter
|
| 37 |
+
set as `weight_scale`, `weight_scale_2`, and `input_scale`.
|
| 38 |
+
- ComfyUI quantization metadata handling:
|
| 39 |
+
https://github.com/Comfy-Org/ComfyUI/blob/5aa71b9bc28809a16596bb9fa3d0a6300d8e3f0e/comfy/utils.py#L1360-L1421
|
| 40 |
+
- This shows that `_quantization_metadata.layers` is converted into
|
| 41 |
+
`{layer}.comfy_quant` JSON byte tensors and that the presence of
|
| 42 |
+
`.comfy_quant` enables mixed quantized ops.
|
| 43 |
+
- ComfyUI native NVFP4 hardware gate:
|
| 44 |
+
https://github.com/Comfy-Org/ComfyUI/blob/5aa71b9bc28809a16596bb9fa3d0a6300d8e3f0e/comfy/model_management.py#L1877-L1885
|
| 45 |
+
- This returns true only for NVIDIA GPUs with compute capability major
|
| 46 |
+
version >= 10, which is why H100 can validate/load files but is not expected
|
| 47 |
+
to use native Blackwell NVFP4 tensor-core compute.
|
| 48 |
+
|
| 49 |
+
## Format findings
|
| 50 |
+
|
| 51 |
+
The original files in this directory are LightX2V NVFP4 Sparse safetensors. Each
|
| 52 |
+
file has 400 quantized Linear layers with these LightX2V-side tensors:
|
| 53 |
+
|
| 54 |
+
- `{layer}.weight`: packed NVFP4 values in `torch.uint8`
|
| 55 |
+
- `{layer}.weight_scale`: FP8 E4M3 block scale tensor
|
| 56 |
+
- `{layer}.alpha`: scalar post-matmul rescaler
|
| 57 |
+
- `{layer}.input_global_scale`: scalar input scale convention
|
| 58 |
+
|
| 59 |
+
Kijai's model card says the ComfyUI conversion is still NVFP4 and uses the same
|
| 60 |
+
datatype, but changes conventions:
|
| 61 |
+
|
| 62 |
+
- Swap the high/low nibbles in each packed `uint8` weight byte.
|
| 63 |
+
- Keep `{layer}.weight_scale` as-is.
|
| 64 |
+
- Convert `{layer}.alpha * {layer}.input_global_scale` into
|
| 65 |
+
`{layer}.weight_scale_2`.
|
| 66 |
+
- Convert `1 / {layer}.input_global_scale` into `{layer}.input_scale`.
|
| 67 |
+
|
| 68 |
+
ComfyUI's mixed precision loader expects a `{layer}.comfy_quant` tensor
|
| 69 |
+
containing JSON bytes. For NVFP4 it then loads:
|
| 70 |
+
|
| 71 |
+
- `{layer}.weight`
|
| 72 |
+
- `{layer}.weight_scale_2`
|
| 73 |
+
- `{layer}.weight_scale`
|
| 74 |
+
- optional registered parameters such as `{layer}.input_scale`
|
| 75 |
+
|
| 76 |
+
The converted file metadata also includes `_quantization_metadata` with one
|
| 77 |
+
`nvfp4` layer entry per quantized layer so ComfyUI can select mixed precision
|
| 78 |
+
operations for the model.
|
| 79 |
+
|
| 80 |
+
## H100 note
|
| 81 |
+
|
| 82 |
+
The conversion itself does not require a Blackwell GPU; it is a safetensors
|
| 83 |
+
layout conversion. However, Comfy Kitchen documents `TensorCoreNVFP4Layout` as
|
| 84 |
+
requiring SM >= 10.0 / Blackwell for native NVFP4 tensor-core acceleration. H100
|
| 85 |
+
is Hopper, so ComfyUI may disable native NVFP4 compute and run a fallback path.
|
| 86 |
+
|
| 87 |
+
## Script
|
| 88 |
+
|
| 89 |
+
The conversion script is:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
python convert_lightx2v_nvfp4_to_comfy.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
Useful options:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
python convert_lightx2v_nvfp4_to_comfy.py --dry-run
|
| 99 |
+
python convert_lightx2v_nvfp4_to_comfy.py --overwrite
|
| 100 |
+
python convert_lightx2v_nvfp4_to_comfy.py input.safetensors --output-dir /path/to/out
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
The script writes `<original_stem>_comfy.safetensors` and uses a temporary file
|
| 104 |
+
before renaming into place.
|
| 105 |
+
|
| 106 |
+
## Converted outputs
|
| 107 |
+
|
| 108 |
+
- `Wan2.2-I2V-A14B_NVFP4_Sparse_high_comfy.safetensors`
|
| 109 |
+
- `Wan2.2-I2V-A14B_NVFP4_Sparse_low_comfy.safetensors`
|
| 110 |
+
- `Wan2.2-T2V-A14B_NVFP4_Sparse_high_comfy.safetensors`
|
| 111 |
+
- `Wan2.2-T2V-A14B_NVFP4_Sparse_low_comfy.safetensors`
|
| 112 |
+
|
| 113 |
+
For ComfyUI native workflows, place these diffusion model files under:
|
| 114 |
+
|
| 115 |
+
```text
|
| 116 |
+
ComfyUI/models/diffusion_models/
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
The Wan2.2 14B workflows still need the normal text encoder and VAE files in
|
| 120 |
+
their ComfyUI locations.
|
| 121 |
+
|
| 122 |
+
## Verification performed
|
| 123 |
+
|
| 124 |
+
For each converted file:
|
| 125 |
+
|
| 126 |
+
- Tensor count is 2695.
|
| 127 |
+
- `_quantization_metadata` contains 400 quantized layers.
|
| 128 |
+
- `alpha` count is 0.
|
| 129 |
+
- `input_global_scale` count is 0.
|
| 130 |
+
- `input_scale` count is 400.
|
| 131 |
+
- `weight_scale` count is 400.
|
| 132 |
+
- `weight_scale_2` count is 400.
|
| 133 |
+
- `comfy_quant` count is 400.
|
| 134 |
+
- `{layer}.comfy_quant` decodes to `{"format": "nvfp4"}`.
|
| 135 |
+
- A sampled `blocks.0.cross_attn.k.weight` block equals the expected nibble
|
| 136 |
+
swap from the original.
|
| 137 |
+
- The sampled `weight_scale_2` equals `alpha * input_global_scale`.
|
| 138 |
+
- The sampled `input_scale` equals `1 / input_global_scale`.
|
convert_lightx2v_nvfp4_to_comfy.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Convert LightX2V Wan2.2 NVFP4 safetensors to ComfyUI NVFP4 format."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from safetensors import safe_open
|
| 12 |
+
from safetensors.torch import save_file
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
COMFY_QUANT_CONF = {"format": "nvfp4"}
|
| 16 |
+
QUANT_LAYER_CONF = {"format": "nvfp4", "full_precision_matrix_mult": False}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def parse_args() -> argparse.Namespace:
|
| 20 |
+
parser = argparse.ArgumentParser(
|
| 21 |
+
description=(
|
| 22 |
+
"Convert LightX2V Wan2.2 NVFP4 Sparse safetensors into ComfyUI's "
|
| 23 |
+
"native NVFP4 safetensors convention."
|
| 24 |
+
)
|
| 25 |
+
)
|
| 26 |
+
parser.add_argument(
|
| 27 |
+
"inputs",
|
| 28 |
+
nargs="*",
|
| 29 |
+
type=Path,
|
| 30 |
+
help="Input .safetensors files. Defaults to non-_comfy safetensors in the current directory.",
|
| 31 |
+
)
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--output-dir",
|
| 34 |
+
type=Path,
|
| 35 |
+
default=Path("."),
|
| 36 |
+
help="Directory for converted files. Defaults to the current directory.",
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--suffix",
|
| 40 |
+
default="_comfy",
|
| 41 |
+
help="Suffix appended before .safetensors. Defaults to _comfy.",
|
| 42 |
+
)
|
| 43 |
+
parser.add_argument(
|
| 44 |
+
"--overwrite",
|
| 45 |
+
action="store_true",
|
| 46 |
+
help="Overwrite existing converted files.",
|
| 47 |
+
)
|
| 48 |
+
parser.add_argument(
|
| 49 |
+
"--dry-run",
|
| 50 |
+
action="store_true",
|
| 51 |
+
help="Inspect inputs and print planned outputs without writing files.",
|
| 52 |
+
)
|
| 53 |
+
return parser.parse_args()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def default_inputs() -> list[Path]:
|
| 57 |
+
return sorted(
|
| 58 |
+
p
|
| 59 |
+
for p in Path(".").glob("*.safetensors")
|
| 60 |
+
if not p.name.endswith("_comfy.safetensors")
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def output_path(input_path: Path, output_dir: Path, suffix: str) -> Path:
|
| 65 |
+
return output_dir / f"{input_path.stem}{suffix}{input_path.suffix}"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def is_quant_layer(prefix: str, keys: set[str]) -> bool:
|
| 69 |
+
return (
|
| 70 |
+
f"{prefix}.weight" in keys
|
| 71 |
+
and f"{prefix}.weight_scale" in keys
|
| 72 |
+
and f"{prefix}.alpha" in keys
|
| 73 |
+
and f"{prefix}.input_global_scale" in keys
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def quant_layer_prefixes(keys: list[str]) -> list[str]:
|
| 78 |
+
key_set = set(keys)
|
| 79 |
+
prefixes: list[str] = []
|
| 80 |
+
for key in keys:
|
| 81 |
+
if key.endswith(".weight"):
|
| 82 |
+
prefix = key[: -len(".weight")]
|
| 83 |
+
if is_quant_layer(prefix, key_set):
|
| 84 |
+
prefixes.append(prefix)
|
| 85 |
+
return prefixes
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def swap_fp4_nibbles(weight: torch.Tensor) -> torch.Tensor:
|
| 89 |
+
if weight.dtype != torch.uint8:
|
| 90 |
+
raise TypeError(f"Expected uint8 packed NVFP4 weight, got {weight.dtype}")
|
| 91 |
+
low = torch.bitwise_left_shift(torch.bitwise_and(weight, 0x0F), 4)
|
| 92 |
+
high = torch.bitwise_right_shift(weight, 4)
|
| 93 |
+
return torch.bitwise_or(low, high).contiguous()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def comfy_quant_tensor() -> torch.Tensor:
|
| 97 |
+
payload = json.dumps(COMFY_QUANT_CONF).encode("utf-8")
|
| 98 |
+
return torch.tensor(list(payload), dtype=torch.uint8)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def convert_one(input_path: Path, output_path_: Path, overwrite: bool, dry_run: bool) -> None:
|
| 102 |
+
if output_path_.exists() and not overwrite and not dry_run:
|
| 103 |
+
raise FileExistsError(f"{output_path_} exists; pass --overwrite to replace it")
|
| 104 |
+
|
| 105 |
+
with safe_open(input_path, framework="pt", device="cpu") as sf:
|
| 106 |
+
keys = list(sf.keys())
|
| 107 |
+
quant_prefixes = quant_layer_prefixes(keys)
|
| 108 |
+
quant_prefix_set = set(quant_prefixes)
|
| 109 |
+
|
| 110 |
+
if dry_run:
|
| 111 |
+
print(
|
| 112 |
+
f"{input_path} -> {output_path_}: "
|
| 113 |
+
f"{len(keys)} input tensors, {len(quant_prefixes)} quantized layers"
|
| 114 |
+
)
|
| 115 |
+
return
|
| 116 |
+
|
| 117 |
+
tensors: dict[str, torch.Tensor] = {}
|
| 118 |
+
for key in keys:
|
| 119 |
+
if key.endswith(".alpha") or key.endswith(".input_global_scale"):
|
| 120 |
+
prefix = key.rsplit(".", 1)[0]
|
| 121 |
+
if prefix in quant_prefix_set:
|
| 122 |
+
continue
|
| 123 |
+
|
| 124 |
+
if key.endswith(".weight"):
|
| 125 |
+
prefix = key[: -len(".weight")]
|
| 126 |
+
tensor = sf.get_tensor(key)
|
| 127 |
+
if prefix in quant_prefix_set:
|
| 128 |
+
tensors[key] = swap_fp4_nibbles(tensor)
|
| 129 |
+
alpha = sf.get_tensor(f"{prefix}.alpha").to(torch.float32)
|
| 130 |
+
input_global_scale = sf.get_tensor(f"{prefix}.input_global_scale").to(torch.float32)
|
| 131 |
+
tensors[f"{prefix}.weight_scale_2"] = (alpha * input_global_scale).contiguous()
|
| 132 |
+
tensors[f"{prefix}.input_scale"] = (1.0 / input_global_scale).contiguous()
|
| 133 |
+
tensors[f"{prefix}.comfy_quant"] = comfy_quant_tensor()
|
| 134 |
+
else:
|
| 135 |
+
tensors[key] = tensor.contiguous() if not tensor.is_contiguous() else tensor
|
| 136 |
+
continue
|
| 137 |
+
|
| 138 |
+
tensor = sf.get_tensor(key)
|
| 139 |
+
tensors[key] = tensor.contiguous() if not tensor.is_contiguous() else tensor
|
| 140 |
+
|
| 141 |
+
metadata = {
|
| 142 |
+
"format": "pt",
|
| 143 |
+
"_quantization_metadata": json.dumps(
|
| 144 |
+
{"layers": {prefix: QUANT_LAYER_CONF for prefix in quant_prefixes}},
|
| 145 |
+
sort_keys=True,
|
| 146 |
+
),
|
| 147 |
+
}
|
| 148 |
+
tmp_path = output_path_.with_name(f"{output_path_.name}.tmp")
|
| 149 |
+
if tmp_path.exists():
|
| 150 |
+
tmp_path.unlink()
|
| 151 |
+
save_file(tensors, tmp_path, metadata=metadata)
|
| 152 |
+
tmp_path.replace(output_path_)
|
| 153 |
+
print(f"Wrote {output_path_} ({len(tensors)} tensors, {len(quant_prefixes)} quantized layers)")
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def main() -> None:
|
| 157 |
+
args = parse_args()
|
| 158 |
+
inputs = args.inputs or default_inputs()
|
| 159 |
+
if not inputs:
|
| 160 |
+
raise SystemExit("No input .safetensors files found")
|
| 161 |
+
|
| 162 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 163 |
+
for input_path in inputs:
|
| 164 |
+
input_path = input_path.resolve()
|
| 165 |
+
if input_path.suffix != ".safetensors":
|
| 166 |
+
raise ValueError(f"Expected a .safetensors input, got {input_path}")
|
| 167 |
+
out = output_path(input_path, args.output_dir, args.suffix).resolve()
|
| 168 |
+
convert_one(input_path, out, overwrite=args.overwrite, dry_run=args.dry_run)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if __name__ == "__main__":
|
| 172 |
+
main()
|
verify_comfy_nvfp4.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Validate converted Wan2.2 NVFP4 safetensors against ComfyUI expectations."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from safetensors import safe_open
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
REQUIRED_QUANT_KEYS = (
|
| 15 |
+
"weight",
|
| 16 |
+
"weight_scale",
|
| 17 |
+
"weight_scale_2",
|
| 18 |
+
"input_scale",
|
| 19 |
+
"comfy_quant",
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def parse_args() -> argparse.Namespace:
|
| 24 |
+
parser = argparse.ArgumentParser()
|
| 25 |
+
parser.add_argument(
|
| 26 |
+
"files",
|
| 27 |
+
nargs="*",
|
| 28 |
+
type=Path,
|
| 29 |
+
help="Converted *_comfy.safetensors files. Defaults to all *_comfy.safetensors in cwd.",
|
| 30 |
+
)
|
| 31 |
+
parser.add_argument(
|
| 32 |
+
"--source-dir",
|
| 33 |
+
type=Path,
|
| 34 |
+
default=Path("."),
|
| 35 |
+
help="Directory containing the original non-_comfy safetensors for numeric relation checks.",
|
| 36 |
+
)
|
| 37 |
+
return parser.parse_args()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def default_files() -> list[Path]:
|
| 41 |
+
return sorted(Path(".").glob("*_comfy.safetensors"))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def source_for(converted: Path, source_dir: Path) -> Path | None:
|
| 45 |
+
source_name = converted.name.replace("_comfy.safetensors", ".safetensors")
|
| 46 |
+
candidate = source_dir / source_name
|
| 47 |
+
return candidate if candidate.exists() else None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def layer_prefixes_from_metadata(metadata: dict[str, str]) -> list[str]:
|
| 51 |
+
raw = metadata.get("_quantization_metadata")
|
| 52 |
+
if raw is None:
|
| 53 |
+
raise AssertionError("missing _quantization_metadata")
|
| 54 |
+
parsed = json.loads(raw)
|
| 55 |
+
layers = parsed.get("layers")
|
| 56 |
+
if not isinstance(layers, dict):
|
| 57 |
+
raise AssertionError("_quantization_metadata.layers is not an object")
|
| 58 |
+
for prefix, conf in layers.items():
|
| 59 |
+
if conf.get("format") != "nvfp4":
|
| 60 |
+
raise AssertionError(f"{prefix}: expected nvfp4 format, got {conf!r}")
|
| 61 |
+
return sorted(layers)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def check_converted_file(path: Path) -> tuple[int, int]:
|
| 65 |
+
with safe_open(path, framework="pt", device="cpu") as sf:
|
| 66 |
+
keys = set(sf.keys())
|
| 67 |
+
metadata = sf.metadata() or {}
|
| 68 |
+
prefixes = layer_prefixes_from_metadata(metadata)
|
| 69 |
+
|
| 70 |
+
if len(prefixes) != 400:
|
| 71 |
+
raise AssertionError(f"{path.name}: expected 400 quantized layers, got {len(prefixes)}")
|
| 72 |
+
|
| 73 |
+
for prefix in prefixes:
|
| 74 |
+
for suffix in REQUIRED_QUANT_KEYS:
|
| 75 |
+
key = f"{prefix}.{suffix}"
|
| 76 |
+
if key not in keys:
|
| 77 |
+
raise AssertionError(f"{path.name}: missing {key}")
|
| 78 |
+
|
| 79 |
+
if f"{prefix}.alpha" in keys:
|
| 80 |
+
raise AssertionError(f"{path.name}: unexpected LightX2V key {prefix}.alpha")
|
| 81 |
+
if f"{prefix}.input_global_scale" in keys:
|
| 82 |
+
raise AssertionError(f"{path.name}: unexpected LightX2V key {prefix}.input_global_scale")
|
| 83 |
+
|
| 84 |
+
weight = sf.get_tensor(f"{prefix}.weight")
|
| 85 |
+
weight_scale = sf.get_tensor(f"{prefix}.weight_scale")
|
| 86 |
+
weight_scale_2 = sf.get_tensor(f"{prefix}.weight_scale_2")
|
| 87 |
+
input_scale = sf.get_tensor(f"{prefix}.input_scale")
|
| 88 |
+
comfy_quant = sf.get_tensor(f"{prefix}.comfy_quant")
|
| 89 |
+
|
| 90 |
+
if weight.dtype != torch.uint8:
|
| 91 |
+
raise AssertionError(f"{path.name}: {prefix}.weight dtype is {weight.dtype}, expected uint8")
|
| 92 |
+
if weight_scale.dtype != torch.float8_e4m3fn:
|
| 93 |
+
raise AssertionError(
|
| 94 |
+
f"{path.name}: {prefix}.weight_scale dtype is {weight_scale.dtype}, expected float8_e4m3fn"
|
| 95 |
+
)
|
| 96 |
+
if weight_scale_2.dtype != torch.float32 or weight_scale_2.shape != torch.Size([]):
|
| 97 |
+
raise AssertionError(f"{path.name}: {prefix}.weight_scale_2 must be scalar float32")
|
| 98 |
+
if input_scale.dtype != torch.float32 or input_scale.shape != torch.Size([]):
|
| 99 |
+
raise AssertionError(f"{path.name}: {prefix}.input_scale must be scalar float32")
|
| 100 |
+
|
| 101 |
+
marker = json.loads(bytes(comfy_quant.tolist()).decode("utf-8"))
|
| 102 |
+
if marker != {"format": "nvfp4"}:
|
| 103 |
+
raise AssertionError(f"{path.name}: {prefix}.comfy_quant is {marker!r}")
|
| 104 |
+
|
| 105 |
+
if sum(key.endswith(".input_scale") for key in keys) != 400:
|
| 106 |
+
raise AssertionError(f"{path.name}: expected 400 input_scale tensors")
|
| 107 |
+
if sum(key.endswith(".weight_scale_2") for key in keys) != 400:
|
| 108 |
+
raise AssertionError(f"{path.name}: expected 400 weight_scale_2 tensors")
|
| 109 |
+
if sum(key.endswith(".comfy_quant") for key in keys) != 400:
|
| 110 |
+
raise AssertionError(f"{path.name}: expected 400 comfy_quant tensors")
|
| 111 |
+
if sum(key.endswith(".alpha") for key in keys) != 0:
|
| 112 |
+
raise AssertionError(f"{path.name}: alpha keys remain after conversion")
|
| 113 |
+
if sum(key.endswith(".input_global_scale") for key in keys) != 0:
|
| 114 |
+
raise AssertionError(f"{path.name}: input_global_scale keys remain after conversion")
|
| 115 |
+
|
| 116 |
+
return len(keys), len(prefixes)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def check_against_source(converted: Path, source: Path) -> None:
|
| 120 |
+
sample_prefixes = (
|
| 121 |
+
"blocks.0.cross_attn.k",
|
| 122 |
+
"blocks.0.ffn.0",
|
| 123 |
+
"blocks.39.self_attn.v",
|
| 124 |
+
)
|
| 125 |
+
with safe_open(source, framework="pt", device="cpu") as src, safe_open(
|
| 126 |
+
converted, framework="pt", device="cpu"
|
| 127 |
+
) as dst:
|
| 128 |
+
for prefix in sample_prefixes:
|
| 129 |
+
src_weight = src.get_tensor(f"{prefix}.weight")[:16, :16]
|
| 130 |
+
dst_weight = dst.get_tensor(f"{prefix}.weight")[:16, :16]
|
| 131 |
+
expected = torch.bitwise_or(
|
| 132 |
+
torch.bitwise_left_shift(torch.bitwise_and(src_weight, 0x0F), 4),
|
| 133 |
+
torch.bitwise_right_shift(src_weight, 4),
|
| 134 |
+
)
|
| 135 |
+
if not torch.equal(dst_weight, expected):
|
| 136 |
+
raise AssertionError(f"{converted.name}: {prefix}.weight nibble swap mismatch")
|
| 137 |
+
|
| 138 |
+
alpha = src.get_tensor(f"{prefix}.alpha").to(torch.float32)
|
| 139 |
+
input_global_scale = src.get_tensor(f"{prefix}.input_global_scale").to(torch.float32)
|
| 140 |
+
weight_scale_2 = dst.get_tensor(f"{prefix}.weight_scale_2").to(torch.float32)
|
| 141 |
+
input_scale = dst.get_tensor(f"{prefix}.input_scale").to(torch.float32)
|
| 142 |
+
|
| 143 |
+
if not torch.allclose(weight_scale_2, alpha * input_global_scale):
|
| 144 |
+
raise AssertionError(f"{converted.name}: {prefix}.weight_scale_2 mismatch")
|
| 145 |
+
if not torch.allclose(input_scale, 1.0 / input_global_scale):
|
| 146 |
+
raise AssertionError(f"{converted.name}: {prefix}.input_scale mismatch")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def main() -> None:
|
| 150 |
+
args = parse_args()
|
| 151 |
+
files = args.files or default_files()
|
| 152 |
+
if not files:
|
| 153 |
+
raise SystemExit("No *_comfy.safetensors files found")
|
| 154 |
+
|
| 155 |
+
for path in files:
|
| 156 |
+
tensor_count, layer_count = check_converted_file(path)
|
| 157 |
+
source = source_for(path, args.source_dir)
|
| 158 |
+
if source is not None:
|
| 159 |
+
check_against_source(path, source)
|
| 160 |
+
relation = "source relation checks passed"
|
| 161 |
+
else:
|
| 162 |
+
relation = "source file not found; skipped relation checks"
|
| 163 |
+
print(f"OK {path.name}: {tensor_count} tensors, {layer_count} nvfp4 layers, {relation}")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
main()
|
verify_comfy_source_compat.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Check converted NVFP4 files against a local ComfyUI source checkout."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from safetensors import safe_open
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def parse_args() -> argparse.Namespace:
|
| 16 |
+
parser = argparse.ArgumentParser()
|
| 17 |
+
parser.add_argument("comfyui", type=Path, help="Path to a ComfyUI source checkout")
|
| 18 |
+
parser.add_argument(
|
| 19 |
+
"files",
|
| 20 |
+
nargs="*",
|
| 21 |
+
type=Path,
|
| 22 |
+
help="Converted *_comfy.safetensors files. Defaults to all *_comfy.safetensors in cwd.",
|
| 23 |
+
)
|
| 24 |
+
return parser.parse_args()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def read_subset(path: Path, limit_layers: int = 3) -> tuple[dict[str, torch.Tensor], dict[str, str]]:
|
| 28 |
+
state_dict: dict[str, torch.Tensor] = {}
|
| 29 |
+
with safe_open(path, framework="pt", device="cpu") as sf:
|
| 30 |
+
metadata = sf.metadata() or {}
|
| 31 |
+
layers = sorted(json.loads(metadata["_quantization_metadata"])["layers"])[:limit_layers]
|
| 32 |
+
for layer in layers:
|
| 33 |
+
for suffix in ("weight", "weight_scale", "weight_scale_2", "input_scale", "comfy_quant"):
|
| 34 |
+
key = f"{layer}.{suffix}"
|
| 35 |
+
state_dict[key] = sf.get_tensor(key)
|
| 36 |
+
return state_dict, metadata
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def main() -> None:
|
| 40 |
+
args = parse_args()
|
| 41 |
+
sys.path.insert(0, str(args.comfyui.resolve()))
|
| 42 |
+
|
| 43 |
+
import comfy.quant_ops as quant_ops
|
| 44 |
+
import comfy.utils as comfy_utils
|
| 45 |
+
|
| 46 |
+
nvfp4 = quant_ops.QUANT_ALGOS.get("nvfp4")
|
| 47 |
+
assert nvfp4 is not None, "ComfyUI source has no nvfp4 quant algo"
|
| 48 |
+
assert nvfp4["storage_t"] == torch.uint8, nvfp4
|
| 49 |
+
assert {"weight_scale", "weight_scale_2", "input_scale"} <= nvfp4["parameters"], nvfp4
|
| 50 |
+
assert nvfp4["comfy_tensor_layout"] == "TensorCoreNVFP4Layout", nvfp4
|
| 51 |
+
|
| 52 |
+
files = args.files or sorted(Path(".").glob("*_comfy.safetensors"))
|
| 53 |
+
for path in files:
|
| 54 |
+
state_dict, metadata = read_subset(path)
|
| 55 |
+
converted, _ = comfy_utils.convert_old_quants(dict(state_dict), metadata=metadata)
|
| 56 |
+
comfy_quant_keys = sorted(k for k in converted if k.endswith(".comfy_quant"))
|
| 57 |
+
if not comfy_quant_keys:
|
| 58 |
+
raise AssertionError(f"{path.name}: ComfyUI did not expose comfy_quant keys")
|
| 59 |
+
for key in comfy_quant_keys:
|
| 60 |
+
marker = json.loads(bytes(converted[key].tolist()).decode("utf-8"))
|
| 61 |
+
if marker.get("format") != "nvfp4":
|
| 62 |
+
raise AssertionError(f"{path.name}: {key} marker is {marker!r}")
|
| 63 |
+
print(f"OK {path.name}: ComfyUI source recognizes nvfp4 metadata and comfy_quant keys")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|