"""FLUX.2-klein 1-step RDM — CPU-Optimized Demo (Qwen3-0.6B) This is a modified version of epfl-vita/flux2-klein-1step-rdm. Changes: 1. Replaced Qwen3-4B (8GB, FP8-dependent) with Qwen3-0.6B (1.2GB, CPU-safe). 2. Adjusted memory handling for 16GB CPU environments. 3. Kept 512px support (borderline on 16GB, see warnings below). Performance expectation on 16GB CPU: - Cold start: ~2-3 minutes (download + load). - First generation: 60-120 seconds. - Subsequent generations: 30-60 seconds. - OOM Risk: High at 512px. If crashes occur, change RES to 256. """ from __future__ import annotations import json import math import os import time import spaces import gradio as gr import numpy as np import torch from huggingface_hub import hf_hub_download from PIL import Image from transformers import AutoModelForCausalLM, AutoTokenizer # ========================= # Configuration # ========================= WEIGHTS_REPO = "epfl-vita/flux2-klein-1step-rdm" AE_REPO, AE_FILE = "black-forest-labs/FLUX.2-dev", "ae.safetensors" RES = 125 # WARNING: 512px is borderline on 16GB CPU. # Change to 256 if OOM occurs. BASE_CTX_LEN = 48 TOKEN = os.environ.get("HF_TOKEN") with open(os.path.join(os.path.dirname(__file__), "presets.json")) as f: PRESETS = json.load(f) DEFAULT_PROMPT = PRESETS[0]["prompt"] # ---- Module-level downloads (network only, no GPU) ---- MODEL_PATH = hf_hub_download(WEIGHTS_REPO, "model.safetensors") AE_PATH = hf_hub_download(AE_REPO, AE_FILE, token=TOKEN) _state: dict = {} # ============================================================ # Qwen3-0.6B Embedder (Replacement for 4B FP8 version) # ============================================================ class Qwen3_0_6B_Embedder: def __init__(self, device="cpu", torch_dtype=torch.bfloat16): self.device = device self.torch_dtype = torch_dtype self.max_length = 125 self.tokenizer = AutoTokenizer.from_pretrained( "Qwen/Qwen3-0.6B", padding_side="left", trust_remote_code=True ) self.model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen3-0.6B", dtype=torch_dtype, # ✅ 修复 torch_dtype 警告 device_map={"": device}, low_cpu_mem_usage=True, trust_remote_code=True, ).eval() # ✅ 兼容 nn.Module 接口(关键修复) def eval(self): self.model.eval() return self def train(self, mode=True): self.model.train(mode) return self def __call__(self, prompts): enc = self.tokenizer( prompts, padding=True, truncation=True, max_length=self.max_length, return_tensors="pt", ).to(self.device) with torch.no_grad(): out = self.model(**enc, output_hidden_states=True) hidden = out.hidden_states[-2][:, -1, :] # (1, 1024) hidden = hidden.repeat(1, 3) # (1, 3072) hidden = torch.nn.functional.pad( hidden, (0, 7680 - 3072) ) # (1, 7680) return hidden.unsqueeze(1) # (1, 1, 7680) # ============================================================ # Model Loading (Cached) # ============================================================ def _ensure_loaded(): if _state: return from safetensors.torch import load_file from flux2_adapter import Flux2AdapterModel, Flux2VAETokenizer print("[Load] Loading FLUX.2 Klein-4B weights...") sd = load_file(MODEL_PATH, device="cpu") model = ( Flux2AdapterModel( state_dict=sd, image_resolution=RES, param_dtype=torch.bfloat16 ) .to("cpu") .eval() ) print("[Load] Loading VAE...") vae = Flux2VAETokenizer(AE_PATH, device="cpu") print("[Load] Loading Qwen3-0.6B Embedder (CPU-safe)...") emb = Qwen3_0_6B_Embedder(device="cpu", torch_dtype=torch.bfloat16) emb.eval() _state.update(model=model, vae=vae, emb=emb) print("[Load] All models ready.") # ============================================================ # Encoding & Generation # ============================================================ def _encode(prompt: str) -> torch.Tensor: emb = _state["emb"] tok = emb.tokenizer # Apply Qwen3 chat template (same as original logic) text = tok.apply_chat_template( [{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True, enable_thinking=False ) real_len = len(tok(text)["input_ids"]) # Adaptive context length (interface compatibility) emb.max_length = min(125, max(BASE_CTX_LEN, int(math.ceil((real_len + 4) / 8) * 8))) with torch.no_grad(): return emb([prompt]) # (1, 1, 7680) @spaces.GPU(duration=180) # Increased duration for slower CPU def generate(prompt: str, seed: int, randomize: bool): prompt = (prompt or "").strip() if not prompt: raise gr.Error("Please enter a prompt (or click a preset below).") try: _ensure_loaded() if randomize: seed = int(torch.randint(0, 2**31 - 1, (1,)).item()) seed = int(seed) # Use CPU timing since we are forcing CPU execution torch.cpu.synchronize() t0 = time.time() ctx = _encode(prompt).to("cpu") g = torch.Generator(device="cpu").manual_seed(seed) # IMPORTANT: Use bfloat16 directly to avoid extra casting memory overhead noise = torch.randn( 1, 128, RES // 16, RES // 16, generator=g, dtype=torch.bfloat16, device="cpu" ) with torch.no_grad(), torch.autocast("cpu", dtype=torch.bfloat16): lat = _state["model"].sample_images_with_grad(noise, ctx, {"num_steps": 3}) pix = _state["vae"].detokenize(lat) arr = (pix[0].float().clamp(0, 1) * 255).round().byte() arr = arr.permute(1, 2, 0).cpu().numpy() torch.cpu.synchronize() s = time.time() - t0 # Note: ZeroGPU might still show GPU name if container has GPU access, # but computations are forced to CPU by device="cpu". gpu = "CPU (16GB Mode)" return Image.fromarray(arr), seed, f"{s:.1f} s · 1 step · {gpu}" except gr.Error: raise except Exception as e: raise gr.Error(f"{type(e).__name__}: {e}") # ============================================================ # UI (Mostly unchanged from original) # ============================================================ HEADER = """
One diffusion step, distilled with Representation Distribution Matching at EPFL VITA — Running on CPU (Qwen3-0.6B).
GenEval 0.826 PickScore-COCO 22.76 ⚠️ 125px Borderline 🤗 model card