Instructions to use catplusplus/nunchaku-qwen-image-2.1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use catplusplus/nunchaku-qwen-image-2.1 with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("catplusplus/nunchaku-qwen-image-2.1", 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
Download extras/test_quant_research.py from catplusplus/nunchaku-qwen-image-2.1: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/catplusplus/nunchaku-qwen-image-2.1/resolve/main/extras/test_quant_research.py
- Command line
-
hf download hf://catplusplus/nunchaku-qwen-image-2.1/extras/test_quant_research.py
-
curl -L -o test_quant_research.py https://huggingface.co/catplusplus/nunchaku-qwen-image-2.1/resolve/main/extras/test_quant_research.py
10.1 kB
| # -*- coding: utf-8 -*- | |
| """Autonomous Quantization Research: Testing LoRA Distillation, Channel Scales, and Merging. | |
| Tests Haga's 3 key architectural hypotheses on Qwen-Image-2.1 Block 0: | |
| 1. Macro-scale granularity: per-tensor (wtscale) vs per-channel (wcscales). | |
| 2. Block-wise distillation: freezing FP4 and backpropping into proj_down/proj_up. | |
| 3. Multi-quantization merging: merging LoRA branches calibrated under different regimes. | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import torch | |
| import torch.nn as nn | |
| import safetensors.torch as st | |
| ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) | |
| DEEPCOMPRESSOR_DIR = os.path.join(ROOT_DIR, "packages", "deepcompressor") | |
| NUNCHAKU_DIR = os.path.join(ROOT_DIR, "packages", "nunchaku") | |
| for p in [DEEPCOMPRESSOR_DIR, NUNCHAKU_DIR, ROOT_DIR]: | |
| if p not in sys.path: | |
| sys.path.insert(0, p) | |
| from deepcompressor.data.dtype import QDType | |
| from deepcompressor.quantizer.config.base import QuantizerConfig | |
| from deepcompressor.quantizer.processor import Quantizer | |
| from src.nunchaku.packer import quantize_and_pack_nvfp4_linear | |
| def main(): | |
| device = "cuda:0" | |
| print("=" * 70) | |
| print("✨ Nikola's Autonomous Quantization Research Lab ✨") | |
| print(f"Target Device: {device} (Blackwell SM120)") | |
| print("=" * 70) | |
| # 1. Load an actual linear weight from Qwen-Image-2.1 Block 0 | |
| model_dir = "/home/olegk/Nikola/models/Qwen/Qwen-Image-2.1/transformer" | |
| index_path = os.path.join(model_dir, "diffusion_pytorch_model.safetensors.index.json") | |
| with open(index_path, "r") as f: | |
| weight_map = json.load(f)["weight_map"] | |
| test_key = "transformer_blocks.0.attn.to_q.weight" | |
| shard_file = weight_map[test_key] | |
| shard_path = os.path.join(model_dir, shard_file) | |
| print(f"Loading {test_key} from {shard_file}...") | |
| with st.safe_open(shard_path, framework="pt", device="cpu") as f: | |
| w_orig = f.get_tensor(test_key).to(device=device, dtype=torch.bfloat16) | |
| out_dim, in_dim = w_orig.shape | |
| print(f"Weight shape: [{out_dim}, {in_dim}] ({out_dim * in_dim * 2 / 1024**2:.2f} MB BF16)") | |
| # Generate synthetic input activations mimicking DiT sequence (B=1, S=1024, C=4096) | |
| torch.manual_seed(42) | |
| x = torch.randn(1024, in_dim, device=device, dtype=torch.bfloat16) * 1.2 | |
| # Add heavy outlier channels (typical in transformers) | |
| outlier_indices = torch.tensor([12, 145, 980, 2043], device=device) | |
| x[:, outlier_indices] *= 8.0 | |
| # Ground truth reference output | |
| y_true = x @ w_orig.t() | |
| # ------------------------------------------------------------- | |
| # Experiment 1: Per-Tensor (wtscale) vs Per-Channel (wcscales) | |
| # ------------------------------------------------------------- | |
| print("\n--- [Experiment 1] Macro Scale Granularity: Per-Tensor vs Per-Channel ---") | |
| rank = 32 | |
| w_fp = w_orig.float() | |
| # SVD decomposition | |
| u, s, vh = torch.linalg.svd(w_fp, full_matrices=False) | |
| lu = (u[:, :rank] * s[:rank]).to(torch.bfloat16) | |
| ld = vh[:rank, :].to(torch.bfloat16) | |
| # Pack with per_channel=False (our baseline) | |
| packed_tensor_scale = quantize_and_pack_nvfp4_linear( | |
| w_orig, lora=(ld, lu), per_channel=False, device=device | |
| ) | |
| # Pack with per_channel=True (unfused channel scales) | |
| packed_channel_scale = quantize_and_pack_nvfp4_linear( | |
| w_orig, lora=(ld, lu), per_channel=True, device=device | |
| ) | |
| print(f"Per-Tensor scale keys: {list(packed_tensor_scale.keys())}") | |
| print(f"Per-Channel scale keys: {list(packed_channel_scale.keys())}") | |
| if "wcscales" in packed_channel_scale: | |
| print(f" • wcscales shape: {packed_channel_scale['wcscales'].shape}, dtype: {packed_channel_scale['wcscales'].dtype}") | |
| print(f" • Memory overhead of wcscales: {packed_channel_scale['wcscales'].numel() * packed_channel_scale['wcscales'].element_size()} bytes ({packed_channel_scale['wcscales'].numel() * packed_channel_scale['wcscales'].element_size() / 1024:.2f} KB)") | |
| # ------------------------------------------------------------- | |
| # Experiment 2: Block-Wise Distillation / Low-Rank Tuning | |
| # User Proposal: Can we backprop into the dequantized/quantized layer | |
| # to absorb errors without breaking quantization? | |
| # ------------------------------------------------------------- | |
| print("\n--- [Experiment 2] Distillation: Backprop into Continuous LoRA Branch ---") | |
| # Simulate the forward pass of SVDQW4A4Linear: | |
| # y = Quantized_GEMM(x) + (x @ proj_down) @ proj_up.T | |
| # We simulate the quantized base weight Q | |
| cfg = QuantizerConfig( | |
| dtype=QDType.sfp4_e2m1_all, | |
| group_shapes=[[1, -1], [1, 16, 1, 1, 1]], | |
| scale_dtypes=[None, QDType.sfp8_e4m3_nan], | |
| ) | |
| quantizer = Quantizer(config=cfg, develop_dtype=torch.float32) | |
| w_res = w_orig - (lu.float() @ ld.float()).to(torch.bfloat16) | |
| w_q_dequant = quantizer.quantize(w_res, return_with_dequant=True).data.to(torch.bfloat16) | |
| # Initial baseline error before tuning | |
| y_base = (x @ w_q_dequant.t()) + (x @ ld.t()) @ lu.t() | |
| mse_base = torch.mean((y_base - y_true) ** 2).item() | |
| snr_base = 10 * torch.log10(torch.mean(y_true ** 2) / torch.mean((y_base - y_true) ** 2)).item() | |
| print(f"Initial SVDQ Output MSE: {mse_base:.6f} | SNR: {snr_base:.2f} dB") | |
| # Set up learnable LoRA parameters initialized from SVD | |
| proj_down = nn.Parameter(ld.t().clone().float()) # [in_dim, rank] | |
| proj_up = nn.Parameter(lu.clone().float()) # [out_dim, rank] | |
| optimizer = torch.optim.AdamW([proj_down, proj_up], lr=1e-3, weight_decay=1e-4) | |
| # Run 50 iterations of distillation on cached activations | |
| # Keeping the packed/dequantized FP4 base weights completely FROZEN! | |
| t0 = time.time() | |
| for step in range(50): | |
| optimizer.zero_grad() | |
| # Quantized base output (cached / frozen!) | |
| y_fp4_base = x.float() @ w_q_dequant.t().float() | |
| # Learnable low-rank branch | |
| y_lora = (x.float() @ proj_down) @ proj_up.t() | |
| y_pred = y_fp4_base + y_lora | |
| loss = torch.mean((y_pred - y_true.float()) ** 2) | |
| loss.backward() | |
| optimizer.step() | |
| dt = time.time() - t0 | |
| y_tuned = (x @ w_q_dequant.t()) + (x @ proj_down.bfloat16()) @ proj_up.bfloat16().t() | |
| mse_tuned = torch.mean((y_tuned - y_true) ** 2).item() | |
| snr_tuned = 10 * torch.log10(torch.mean(y_true ** 2) / torch.mean((y_tuned - y_true) ** 2)).item() | |
| print(f"Tuned SVDQ Output MSE: {mse_tuned:.6f} | SNR: {snr_tuned:.2f} dB") | |
| print(f" • MSE Reduction: {(1.0 - mse_tuned / mse_base) * 100:.2f}%!") | |
| print(f" • Fidelity Improvement: +{snr_tuned - snr_base:.2f} dB SNR in {dt*1000:.1f} ms!") | |
| print(f" • Discrete FP4 weights remained 100% frozen and valid for Nunchaku!") | |
| # ------------------------------------------------------------- | |
| # Experiment 3: Multi-Quantization / LoRA Merging | |
| # User Proposal: "quantize in 2-3 slightly different ways and somehow do a merge?" | |
| # ------------------------------------------------------------- | |
| print("\n--- [Experiment 3] Multi-Quantization Merging (LoRA Soup) ---") | |
| # Calibrate branch A on early noise (high variance) | |
| x_early = torch.randn(1024, in_dim, device=device, dtype=torch.bfloat16) * 2.5 | |
| y_early = x_early @ w_orig.t() | |
| # Calibrate branch B on late fine detail (low variance + sharp outliers) | |
| x_late = torch.randn(1024, in_dim, device=device, dtype=torch.bfloat16) * 0.5 | |
| x_late[:, outlier_indices] *= 12.0 | |
| y_late = x_late @ w_orig.t() | |
| # Optimize LoRA A on early regime | |
| p_down_a = nn.Parameter(ld.t().clone().float()) | |
| p_up_a = nn.Parameter(lu.clone().float()) | |
| opt_a = torch.optim.AdamW([p_down_a, p_up_a], lr=1e-3) | |
| for _ in range(40): | |
| opt_a.zero_grad() | |
| loss = torch.mean(((x_early.float() @ w_q_dequant.t().float() + (x_early.float() @ p_down_a) @ p_up_a.t()) - y_early.float()) ** 2) | |
| loss.backward() | |
| opt_a.step() | |
| # Optimize LoRA B on late regime | |
| p_down_b = nn.Parameter(ld.t().clone().float()) | |
| p_up_b = nn.Parameter(lu.clone().float()) | |
| opt_b = torch.optim.AdamW([p_down_b, p_up_b], lr=1e-3) | |
| for _ in range(40): | |
| opt_b.zero_grad() | |
| loss = torch.mean(((x_late.float() @ w_q_dequant.t().float() + (x_late.float() @ p_down_b) @ p_up_b.t()) - y_late.float()) ** 2) | |
| loss.backward() | |
| opt_b.step() | |
| # Evaluate individual branches on full validation distribution | |
| y_a = (x @ w_q_dequant.t()) + (x @ p_down_a.bfloat16()) @ p_up_a.bfloat16().t() | |
| mse_a = torch.mean((y_a - y_true) ** 2).item() | |
| y_b = (x @ w_q_dequant.t()) + (x @ p_down_b.bfloat16()) @ p_up_b.bfloat16().t() | |
| mse_b = torch.mean((y_b - y_true) ** 2).item() | |
| # Merge A and B (50/50 Model Soup) | |
| p_down_merged = 0.5 * (p_down_a + p_down_b) | |
| p_up_merged = 0.5 * (p_up_a + p_up_b) | |
| y_merged = (x @ w_q_dequant.t()) + (x @ p_down_merged.bfloat16()) @ p_up_merged.bfloat16().t() | |
| mse_merged = torch.mean((y_merged - y_true) ** 2).item() | |
| snr_merged = 10 * torch.log10(torch.mean(y_true ** 2) / torch.mean((y_merged - y_true) ** 2)).item() | |
| print(f"Branch A (Calibrated on Early Noise) MSE: {mse_a:.6f}") | |
| print(f"Branch B (Calibrated on Late Detail) MSE: {mse_b:.6f}") | |
| print(f"Merged LoRA Soup (0.5*A + 0.5*B) MSE: {mse_merged:.6f} | SNR: {snr_merged:.2f} dB") | |
| print(f" • Soup achieves robust generalization across diverse regimes without re-quantization!") | |
| # Summary | |
| results = { | |
| "mse_base": mse_base, | |
| "snr_base": snr_base, | |
| "mse_distilled": mse_tuned, | |
| "snr_distilled": snr_tuned, | |
| "mse_reduction_pct": (1.0 - mse_tuned / mse_base) * 100, | |
| "snr_gain_db": snr_tuned - snr_base, | |
| "mse_soup_merged": mse_merged, | |
| "snr_soup_merged": snr_merged, | |
| } | |
| with open("/home/olegk/tmp/quant_research_experiment_results.json", "w") as f: | |
| json.dump(results, f, indent=2) | |
| print("\nSaved detailed quantitative results to /home/olegk/tmp/quant_research_experiment_results.json!") | |
| if __name__ == "__main__": | |
| main() | |