multimodalart's picture
multimodalart HF Staff
Port fast-demo UI layout + prompt upscaling (Qwen3-VL) toggle
1ceb4e3 verified
Raw
History Blame Contribute Delete
13.4 kB
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
# outlines_core ships an @torch.compile bitmask kernel dynamo can't trace -> noisy WON'T CONVERT
# spam on every local upsample. We never torch.compile at runtime, so disable dynamo.
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
import json
import math
import random
import time
import gradio as gr
import spaces
import torch
from diffusers import Ideogram4Pipeline, Ideogram4Transformer2DModel
try:
from diffusers import Ideogram4PromptEnhancerHead
_HAS_HEAD = True
except Exception: # pragma: no cover - older diffusers without the enhancer head
_HAS_HEAD = False
# Runtime shim: cu130-era bitsandbytes returns Params4bit.shape as a plain tuple,
# but diffusers' check_quantized_param_shape calls .numel() on it. math.prod handles both.
from diffusers.quantizers.bitsandbytes.bnb_quantizer import BnB4BitDiffusersQuantizer
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
MODEL_ID = "fal/ideogram-v4-instant"
COMPONENTS_REPO = "ideogram-ai/ideogram-4-nf4-diffusers"
COMPONENTS_REVISION = "1874bc70267ba2c823a7239e1d70dd308c8d64dc"
LM_HEAD_REPO = "diffusers/qwen3-vl-8b-instruct-lm-head"
HF_TOKEN = os.environ.get("HF_TOKEN") # components repo is gated -> read it with the Space secret
MAX_SEED = 2**31 - 1
# AoTI: a precompiled ZeroGPUCompiledModel of the conditional transformer,
# produced offline by the compile Space and stored as a .pt2 blob. Loading it
# here and applying with spaces.aoti_apply avoids compiling per-request.
AOTI_REPO = "hugging-apps/ideogram-v4-instant-aoti"
AOTI_PT2_FILENAME = "transformer.pt2"
# Aspect-ratio presets -> (width, height). All multiples of 64.
ASPECT_RATIOS = {
"1:1 · 1024×1024": (1024, 1024),
"3:2 · 1216×832": (1216, 832),
"2:3 · 832×1216": (832, 1216),
"16:9 · 1344×768": (1344, 768),
"9:16 · 768×1344": (768, 1344),
}
DEFAULT_RATIO = "1:1 · 1024×1024"
# --- Load model at module scope, .to("cuda") eagerly ---
# The fal/ideogram-v4-instant transformer is BF16 with split QKV weights.
# We load the full NF4 pipeline (which has both transformers), dequantize them,
# then override the conditional transformer with the fal instant checkpoint.
# guidance_schedule=[1.0]*8 makes the unconditional pass a no-op (v = pos_v),
# matching the model card's no-CFG single-branch behavior.
# Optional local prompt enhancer (Qwen3-VL LM head grafted onto the text encoder). Free, on-device.
enhancer_head = None
if _HAS_HEAD:
try:
enhancer_head = Ideogram4PromptEnhancerHead.from_pretrained(
LM_HEAD_REPO, torch_dtype=torch.bfloat16, token=HF_TOKEN
)
except Exception as e:
print(f"[enhancer] LM-head load failed (raw prompt only): {e!r}", flush=True)
t0 = time.perf_counter()
pipe = Ideogram4Pipeline.from_pretrained(
COMPONENTS_REPO,
revision=COMPONENTS_REVISION,
prompt_enhancer_head=enhancer_head,
torch_dtype=torch.bfloat16,
token=HF_TOKEN,
)
pipe.transformer.dequantize()
pipe.unconditional_transformer.dequantize()
# Override the conditional transformer with the fal instant checkpoint
instant_transformer = Ideogram4Transformer2DModel.from_pretrained(
MODEL_ID,
subfolder="transformer",
torch_dtype=torch.bfloat16,
token=HF_TOKEN,
)
pipe.transformer = instant_transformer
pipe.to("cuda")
print(f"[timing] pipeline load: {time.perf_counter() - t0:.1f}s", flush=True)
# --- Load the AoTI-compiled conditional transformer and apply it before serving ---
# Download the precompiled ZeroGPUCompiledModel (.pt2 blob) and swap it in with
# spaces.aoti_apply, so requests never pay a compile cost. Falls back to the eager
# transformer if the artifact is unavailable or incompatible.
try:
import pickle
from huggingface_hub import hf_hub_download
t_aoti = time.perf_counter()
_pt2_path = hf_hub_download(
repo_id=AOTI_REPO,
filename=AOTI_PT2_FILENAME,
repo_type="model",
)
with open(_pt2_path, "rb") as _f:
_compiled_transformer = pickle.load(_f)
spaces.aoti_apply(_compiled_transformer, pipe.transformer)
print(
f"[timing] AoTI apply: {time.perf_counter() - t_aoti:.1f}s "
f"({type(_compiled_transformer).__name__})",
flush=True,
)
except Exception as e:
print(f"AoTI load failed ({e!r}); running eager transformer", flush=True)
# No-CFG guidance schedule: all ones means v = pos_v (conditional only)
NO_CFG_SCHEDULE = (1.0,) * 8
def _looks_like_json(text):
s = (text or "").strip()
return s.startswith("{") and s.endswith("}")
def _build_prompt(text_prompt: str) -> str:
"""Wrap a natural-language prompt into Ideogram 4's JSON caption format."""
return json.dumps(
{"high_level_description": text_prompt},
ensure_ascii=False,
separators=(",", ":"),
)
# --- Warm the local prompt enhancer on the startup worker (forks inherit the graft) --------------------
@spaces.GPU(duration=120)
def _warmup():
if enhancer_head is not None:
pipe.upsample_prompt("a red apple on a wooden table", height=1024, width=1024)
if enhancer_head is not None:
try:
_warmup()
print("[enhancer] prompt enhancer grafted", flush=True)
except Exception as e:
print(f"[enhancer] warmup failed (will graft lazily on first request): {e!r}", flush=True)
def _gpu_duration(prompt, aspect_ratio=DEFAULT_RATIO, enhance=True, seed=0, randomize_seed=True, *args, **kwargs):
"""Estimate GPU duration based on image resolution (8 fixed steps, 2 transformer passes)."""
_TOK_1024 = (1024 // 16) ** 2 # 4096 image tokens
_TOK_2048 = (2048 // 16) ** 2 # 16384
_PS_1024 = 1.0 / 1.10 # ~0.91 s/it per transformer
_PS_2048 = 6.0 # 6 s/it per transformer
_PS_B = (_PS_2048 - _PS_1024) / (_TOK_2048 - _TOK_1024)
_PS_A = _PS_1024 - _PS_B * _TOK_1024
width, height = ASPECT_RATIOS.get(aspect_ratio, ASPECT_RATIOS[DEFAULT_RATIO])
tok = (int(width) // 16) * (int(height) // 16)
per_step = max(0.2, _PS_A + _PS_B * tok)
# 2 transformer calls per step (conditional + unconditional, even though uncond is a no-op)
budget = 8 * per_step * 2 + 12 # 12s for text encoding + VAE + cold-start overhead
if enhance:
budget += 20 # local prompt upsampling (grafted Qwen head)
return max(30, min(240, int(math.ceil(budget * 1.3))))
@spaces.GPU(duration=_gpu_duration)
def generate(
prompt: str,
aspect_ratio: str = DEFAULT_RATIO,
enhance: bool = True,
seed: int = 0,
randomize_seed: bool = True,
progress=gr.Progress(track_tqdm=True),
):
"""Generate an image from a text prompt using Ideogram 4 Instant.
An 8-step distilled text-to-image model by fal with no runtime CFG, producing
high-quality images—including text rendering—in seconds. Ideogram 4 is trained
on structured JSON captions, so a plain prompt is optionally expanded into one
on-device (Qwen3-VL) before generation — or paste your own JSON caption.
Args:
prompt: A plain-text prompt (expanded into Ideogram's JSON caption when `enhance` is on) or a
complete structured JSON caption fed to the model verbatim.
aspect_ratio: One of the preset aspect-ratio / resolution labels.
enhance: Expand a plain-text prompt into Ideogram's structured JSON caption before generation.
seed: RNG seed (ignored when `randomize_seed` is on).
randomize_seed: Draw a fresh random seed each run.
"""
if not prompt or not prompt.strip():
raise gr.Error("Please enter a prompt.")
if randomize_seed or seed is None or int(seed) < 0:
seed = random.randint(0, MAX_SEED)
seed = int(seed)
width, height = ASPECT_RATIOS.get(aspect_ratio, ASPECT_RATIOS[DEFAULT_RATIO])
generator = torch.Generator(device="cuda").manual_seed(seed)
# Ideogram 4 is trained on structured JSON captions. If the user typed JSON, honour it verbatim;
# otherwise (when `enhance` is on and the enhancer is available) upsample the plain prompt into a
# native caption with the on-device Qwen3-VL head. Toggle off to feed a minimal JSON wrapper.
text = prompt.strip()
if _looks_like_json(text):
final_prompt = text # already a JSON caption
elif enhance and enhancer_head is not None:
progress(0.0, desc="✍️ Writing the JSON caption…")
try:
final_prompt = pipe.upsample_prompt(
text, height=height, width=width, generator=generator
)[0]
except Exception as e:
print(f"[enhancer] failed, using raw prompt: {e!r}", flush=True)
gr.Warning("Prompt enhancer unavailable — generating from the raw prompt.")
final_prompt = _build_prompt(text)
else:
final_prompt = _build_prompt(text)
progress(0.0, desc="🎨 Generating…")
t = time.perf_counter()
image = pipe(
prompt=final_prompt,
height=height,
width=width,
num_inference_steps=8,
guidance_schedule=NO_CFG_SCHEDULE,
mu=0.0,
std=1.75,
generator=generator,
).images[0]
dt = time.perf_counter() - t
print(f"[timing] diffusion (8 steps, {width}x{height}): {dt:.2f}s", flush=True)
try:
caption = json.loads(final_prompt)
except (TypeError, ValueError):
caption = {"prompt": final_prompt}
return image, seed, caption, f"8 steps · {dt:.1f}s"
CSS = """
#col-container { max-width: 1200px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
DEFAULT_PROMPT = "a ginger cat wearing a tiny wizard hat reading a spellbook"
with gr.Blocks(title="Ideogram 4 Instant · by fal") as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown(
"# Ideogram 4 Instant ⚡ — by fal\n"
"[**fal/ideogram-v4-instant**](https://huggingface.co/fal/ideogram-v4-instant) is a speed-distilled "
"Ideogram 4 checkpoint: **8 steps, no runtime CFG**. Ideogram 4 is trained on "
"**structured JSON captions**, so a plain prompt is expanded into one on-device (Qwen3-VL) before "
"generation — or paste your own JSON caption.\n\n"
"[Model](https://huggingface.co/fal/ideogram-v4-instant) · "
"[Base Ideogram 4](https://huggingface.co/ideogram-ai/ideogram-4-nf4-diffusers) · "
"[fal blog](https://blog.fal.ai/serving-sub-second-ideogram-v4-without-quality-loss/)"
)
with gr.Row():
with gr.Column():
prompt = gr.Textbox(
label="Prompt",
value=DEFAULT_PROMPT,
lines=3,
info="Plain text (auto-expanded to a JSON caption) or a full structured JSON caption.",
)
run = gr.Button("Generate", variant="primary")
aspect_ratio = gr.Radio(
choices=list(ASPECT_RATIOS.keys()), value=DEFAULT_RATIO, label="Aspect ratio"
)
with gr.Accordion("Advanced settings", open=False):
enhance = gr.Checkbox(
label="Enhance prompt → JSON caption",
value=True,
info="Ideogram 4 is trained on structured captions. On = best quality (recommended). "
"Ignored when the prompt is already JSON.",
)
with gr.Row():
seed = gr.Number(label="Seed", value=0, precision=0)
randomize = gr.Checkbox(label="Randomize seed", value=True)
with gr.Column():
out_image = gr.Image(label="Output", type="pil")
with gr.Row():
out_seed = gr.Number(label="Seed used", precision=0, interactive=False)
out_time = gr.Textbox(label="Generation", interactive=False)
out_caption = gr.JSON(label="Caption fed to the model")
gr.Examples(
examples=[
["a ginger cat wearing a tiny wizard hat reading a spellbook"],
["A bold typographic poster with the words HELLO WORLD in vibrant gradient colors"],
["an isometric illustration of a tiny city floating in the clouds"],
["a cinematic photo of a golden retriever puppy in a field of sunflowers at golden hour"],
],
inputs=[prompt],
outputs=[out_image, out_seed, out_caption, out_time],
fn=generate,
cache_examples=True,
cache_mode="lazy",
)
run.click(
generate,
inputs=[prompt, aspect_ratio, enhance, seed, randomize],
outputs=[out_image, out_seed, out_caption, out_time],
api_name="generate",
)
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)