Spaces:
Running on Zero
Running on Zero
| import json | |
| import math | |
| import os | |
| import random | |
| import time | |
| from threading import Lock, Thread | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| os.environ.setdefault("TORCHDYNAMO_DISABLE", "1") | |
| import gradio as gr | |
| import spaces | |
| import torch | |
| from diffusers import Ideogram4Pipeline | |
| from diffusers.quantizers.bitsandbytes.bnb_quantizer import BnB4BitDiffusersQuantizer | |
| from huggingface_hub import hf_hub_download | |
| MODEL_ID = "ideogram-ai/ideogram-4-nf4" | |
| LM_HEAD_REPO = "multimodalart/qwen3-vl-8b-instruct-lm-head" | |
| AOTI_REPO = "multimodalart/i4-block-aoti" | |
| MAX_SEED = 2**31 - 1 | |
| def _check_quantized_param_shape(self, param_name, current_param, loaded_param): | |
| n = math.prod(tuple(current_param.shape)) | |
| inferred_shape = (n,) if "bias" in param_name else ((n + 1) // 2, 1) | |
| if tuple(loaded_param.shape) != tuple(inferred_shape): | |
| raise ValueError( | |
| f"Expected flattened shape of {param_name} to be {inferred_shape}, " | |
| f"got {tuple(loaded_param.shape)}." | |
| ) | |
| return True | |
| BnB4BitDiffusersQuantizer.check_quantized_param_shape = _check_quantized_param_shape | |
| pipe = None | |
| pipe_lock = Lock() | |
| _AOTI_APPLIED = False | |
| _AOTI_OK = False | |
| def get_pipe(): | |
| global pipe, _AOTI_OK | |
| if pipe is not None: | |
| return pipe | |
| with pipe_lock: | |
| if pipe is None: | |
| print(f"Loading {MODEL_ID} pipeline...", flush=True) | |
| t = time.perf_counter() | |
| token = os.environ.get("HF_TOKEN") or None | |
| loaded_pipe = Ideogram4Pipeline.from_pretrained( | |
| MODEL_ID, | |
| torch_dtype=torch.bfloat16, | |
| token=token, | |
| ) | |
| loaded_pipe.transformer.dequantize() | |
| loaded_pipe.unconditional_transformer.dequantize() | |
| loaded_pipe.to("cuda") | |
| pipe = loaded_pipe | |
| print(f"Pipeline loaded in {time.perf_counter() - t:.1f}s.", flush=True) | |
| try: | |
| hf_hub_download(AOTI_REPO, "package.pt2", subfolder="Ideogram4TransformerBlock") | |
| from torch._inductor.cpu_vec_isa import valid_vec_isa_list | |
| valid_vec_isa_list() | |
| _AOTI_OK = True | |
| except Exception as e: | |
| _AOTI_OK = False | |
| print(f"[aoti] prefetch/prewarm failed, running eager: {e!r}", flush=True) | |
| return pipe | |
| def apply_aoti(): | |
| global _AOTI_APPLIED | |
| if _AOTI_APPLIED or not _AOTI_OK or pipe is None: | |
| return | |
| try: | |
| t = time.perf_counter() | |
| spaces.aoti_blocks_load(pipe.transformer, AOTI_REPO) | |
| spaces.aoti_blocks_load(pipe.unconditional_transformer, AOTI_REPO) | |
| _AOTI_APPLIED = True | |
| print(f"AOTI blocks loaded in {time.perf_counter() - t:.2f}s.", flush=True) | |
| except Exception as e: | |
| print(f"[aoti] apply failed, running eager: {e!r}", flush=True) | |
| _TOK_1024 = (1024 // 16) ** 2 | |
| _TOK_2048 = (2048 // 16) ** 2 | |
| _PS_1024 = 1.0 / 1.10 | |
| _PS_2048 = 6.0 | |
| _PS_B = (_PS_2048 - _PS_1024) / (_TOK_2048 - _TOK_1024) | |
| _PS_A = _PS_1024 - _PS_B * _TOK_1024 | |
| LOCAL_UPSAMPLE_S = 15 | |
| DIFFUSION_OVERHEAD_S = 10 | |
| DURATION_MARGIN = 1.35 | |
| def per_step(width, height): | |
| tokens = (int(width) // 16) * (int(height) // 16) | |
| return max(0.2, _PS_A + _PS_B * tokens) | |
| def gpu_duration( | |
| final_prompt, | |
| width, | |
| height, | |
| num_inference_steps, | |
| guidance_scale, | |
| image_count, | |
| seed, | |
| use_magic_prompt, | |
| progress=None, | |
| ): | |
| budget = int(num_inference_steps) * per_step(width, height) * int(image_count) | |
| budget += DIFFUSION_OVERHEAD_S | |
| if use_magic_prompt: | |
| budget += LOCAL_UPSAMPLE_S | |
| return max(60, int(math.ceil(budget * DURATION_MARGIN))) | |
| def normalize_seed(seed): | |
| base_seed = int(seed) % MAX_SEED | |
| if base_seed < 0: | |
| base_seed += MAX_SEED | |
| return base_seed | |
| def build_prompt(prompt, negative_prompt): | |
| prompt = (prompt or "").strip() | |
| if not prompt: | |
| raise gr.Error("Please enter a prompt.") | |
| negative_prompt = (negative_prompt or "").strip() | |
| if negative_prompt: | |
| return f"{prompt}\nAvoid: {negative_prompt}" | |
| return prompt | |
| def build_sampler_kwargs(num_inference_steps, guidance_scale): | |
| steps = max(1, int(num_inference_steps)) | |
| guidance = float(guidance_scale) | |
| polish_steps = max(1, round(steps * 0.1)) | |
| main_steps = max(0, steps - polish_steps) | |
| guidance_schedule = (guidance,) * main_steps + (3.0,) * polish_steps | |
| mu = 0.5 if steps <= 12 else 0.0 | |
| std = 1.5 if steps >= 48 else 1.75 | |
| return { | |
| "num_inference_steps": steps, | |
| "guidance_schedule": guidance_schedule, | |
| "mu": mu, | |
| "std": std, | |
| } | |
| def gpu_generate( | |
| final_prompt, | |
| width, | |
| height, | |
| num_inference_steps, | |
| guidance_scale, | |
| image_count, | |
| seed, | |
| use_magic_prompt, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| if not torch.cuda.is_available(): | |
| raise RuntimeError("CUDA is not available inside the ZeroGPU worker.") | |
| ideogram_pipe = get_pipe() | |
| aoti_thread = Thread(target=apply_aoti, daemon=True) | |
| aoti_thread.start() | |
| caption = final_prompt | |
| if use_magic_prompt: | |
| progress(0.0, desc="Upsampling prompt with local Qwen...") | |
| try: | |
| t = time.perf_counter() | |
| caption = ideogram_pipe.upsample_prompt( | |
| final_prompt, | |
| height=int(height), | |
| width=int(width), | |
| lm_head_repo_id=LM_HEAD_REPO, | |
| )[0] | |
| print(f"Prompt upsampled in {time.perf_counter() - t:.2f}s.", flush=True) | |
| except Exception as e: | |
| print(f"[upsample] local failed: {e!r}", flush=True) | |
| gr.Warning("Local prompt upsampler failed. Generating from the raw prompt.") | |
| aoti_thread.join() | |
| images = [] | |
| seeds = [(normalize_seed(seed) + i) % MAX_SEED for i in range(int(image_count))] | |
| sampler_kwargs = build_sampler_kwargs(num_inference_steps, guidance_scale) | |
| for current_seed in seeds: | |
| progress(0.0, desc=f"Generating image with seed {current_seed}...") | |
| generator = torch.Generator(device="cuda").manual_seed(int(current_seed)) | |
| image = ideogram_pipe( | |
| prompt=caption, | |
| width=int(width), | |
| height=int(height), | |
| generator=generator, | |
| **sampler_kwargs, | |
| ).images[0] | |
| images.append(image) | |
| try: | |
| parsed_caption = json.loads(caption) | |
| except Exception: | |
| parsed_caption = {"prompt": caption} | |
| return images, ", ".join(str(s) for s in seeds), parsed_caption | |
| def generate_image( | |
| prompt, | |
| negative_prompt, | |
| height, | |
| width, | |
| num_inference_steps, | |
| guidance_scale, | |
| image_count, | |
| seed, | |
| randomize_seed, | |
| use_magic_prompt, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| if randomize_seed: | |
| seed = random.randint(0, MAX_SEED) | |
| final_prompt = build_prompt(prompt, negative_prompt) | |
| return gpu_generate( | |
| final_prompt, | |
| int(width), | |
| int(height), | |
| int(num_inference_steps), | |
| float(guidance_scale), | |
| int(image_count), | |
| normalize_seed(seed), | |
| bool(use_magic_prompt), | |
| ) | |
| examples = [ | |
| [ | |
| 'A clean product poster for a premium coffee brand, exact text "MORNING RITUAL", warm studio lighting, elegant typography, realistic packaging' | |
| ], | |
| [ | |
| 'A movie poster for a retro sci-fi film, exact title text "ORBIT CITY", bold typography, neon lights, cinematic composition' | |
| ], | |
| [ | |
| "A detailed editorial photo of a ceramic artist studio, handmade bowls, soft window light, natural colors, realistic texture" | |
| ], | |
| [ | |
| 'A square app icon for a weather app, exact text "SKYCAST", minimal vector style, blue and yellow palette, crisp edges' | |
| ], | |
| ] | |
| with gr.Blocks(title="Ideogram 4 NF4 Demo") as demo: | |
| gr.Markdown( | |
| """ | |
| # Ideogram 4 NF4 Demo | |
| Generate images using [ideogram-ai/ideogram-4-nf4](https://huggingface.co/ideogram-ai/ideogram-4-nf4) on ZeroGPU. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| prompt = gr.Textbox( | |
| label="Prompt", | |
| placeholder="Enter your image description...", | |
| lines=4, | |
| ) | |
| negative_prompt = gr.Textbox( | |
| label="Negative Prompt", | |
| placeholder="Things you want the model to avoid...", | |
| lines=3, | |
| ) | |
| with gr.Row(): | |
| height = gr.Slider( | |
| minimum=512, | |
| maximum=2048, | |
| value=1024, | |
| step=64, | |
| label="Height", | |
| ) | |
| width = gr.Slider( | |
| minimum=512, | |
| maximum=2048, | |
| value=1024, | |
| step=64, | |
| label="Width", | |
| ) | |
| with gr.Row(): | |
| num_inference_steps = gr.Slider( | |
| minimum=8, | |
| maximum=48, | |
| value=20, | |
| step=1, | |
| label="Inference Steps", | |
| info="Official presets are 12, 20, and 48 steps.", | |
| ) | |
| image_count = gr.Slider( | |
| minimum=1, | |
| maximum=2, | |
| value=1, | |
| step=1, | |
| label="Images", | |
| ) | |
| guidance_scale = gr.Slider( | |
| minimum=1.0, | |
| maximum=10.0, | |
| value=7.0, | |
| step=0.1, | |
| label="CFG Guidance Scale", | |
| info="Ideogram 4 default main guidance is 7.0.", | |
| ) | |
| use_magic_prompt = gr.Checkbox( | |
| label="Use Local Magic Prompt", | |
| value=True, | |
| info="Rewrites plain text into Ideogram's structured JSON prompt.", | |
| ) | |
| with gr.Row(): | |
| seed = gr.Number( | |
| label="Seed", | |
| value=0, | |
| precision=0, | |
| ) | |
| randomize_seed = gr.Checkbox( | |
| label="Randomize Seed", | |
| value=True, | |
| ) | |
| generate_btn = gr.Button("Generate", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| output_images = gr.Gallery( | |
| label="Generated Images", | |
| columns=2, | |
| rows=2, | |
| preview=True, | |
| ) | |
| used_seeds = gr.Textbox( | |
| label="Seeds Used", | |
| interactive=False, | |
| ) | |
| caption_json = gr.JSON( | |
| label="Caption Sent To Model", | |
| ) | |
| gr.Markdown("### Example Prompts") | |
| gr.Examples( | |
| examples=examples, | |
| inputs=[prompt], | |
| cache_examples=False, | |
| ) | |
| gr.Markdown( | |
| "Model by Ideogram. Ideogram 4 NF4 is gated and uses the Ideogram 4 Non-Commercial license." | |
| ) | |
| inputs = [ | |
| prompt, | |
| negative_prompt, | |
| height, | |
| width, | |
| num_inference_steps, | |
| guidance_scale, | |
| image_count, | |
| seed, | |
| randomize_seed, | |
| use_magic_prompt, | |
| ] | |
| outputs = [output_images, used_seeds, caption_json] | |
| generate_btn.click( | |
| fn=generate_image, | |
| inputs=inputs, | |
| outputs=outputs, | |
| ) | |
| prompt.submit( | |
| fn=generate_image, | |
| inputs=inputs, | |
| outputs=outputs, | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(show_error=True) | |