File size: 6,023 Bytes
d9a4123
93bd5a0
23df166
 
 
 
 
 
 
d9a4123
23df166
 
 
 
d9a4123
23df166
1cfdf94
883e411
d9a4123
1cfdf94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23df166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533ee5a
955087d
23df166
6822ae0
23df166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
955087d
 
 
 
23df166
 
 
 
 
 
955087d
23df166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d9a4123
23df166
 
 
 
 
 
 
d9a4123
 
 
 
93bd5a0
d9a4123
 
93bd5a0
 
883e411
 
23df166
d9a4123
 
 
 
 
23df166
d9a4123
23df166
93bd5a0
 
b894812
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import os

os.environ.setdefault("PYTHONUNBUFFERED", "1")
os.environ.setdefault("GRADIO_SSR_MODE", "0")
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")

# spaces MUST be imported before torch / diffusers so ZeroGPU can
# monkey-patch CUDA. This is how the working Ideogram 4 Spaces boot.
import spaces
import math
import sys

import torch
import gradio as gr
from diffusers import Ideogram4Pipeline
from diffusers.quantizers.bitsandbytes.bnb_quantizer import BnB4BitDiffusersQuantizer
from PIL import Image

# cu130-era bitsandbytes returns Params4bit.shape as a plain tuple.
# diffusers' check_quantized_param_shape calls .numel() on it and crashes
# with: AttributeError: 'tuple' object has no attribute 'numel'
# Same shim as linoyts/ideogram-4-turbotime.
def _check_quantized_param_shape(self, param_name, current_param, loaded_param):
    current_shape = getattr(current_param, "shape", current_param)
    loaded_shape = getattr(loaded_param, "shape", loaded_param)
    n = math.prod(tuple(current_shape))
    inferred_shape = (n,) if "bias" in param_name else ((n + 1) // 2, 1)
    if tuple(loaded_shape) != tuple(inferred_shape):
        raise ValueError(
            f"Expected flattened shape of {param_name} to be {inferred_shape}, "
            f"got {tuple(loaded_shape)}."
        )
    return True


BnB4BitDiffusersQuantizer.check_quantized_param_shape = _check_quantized_param_shape

MODEL_ID = "ideogram-ai/ideogram-4-nf4"
LORA_REPO = "ostris/ideogram_4_turbotime_lora"
LORA_FILE = "ideogram_4_turbotime_v1.safetensors"
HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")


def log(msg: str) -> None:
    print(msg, file=sys.stderr, flush=True)


if not HF_TOKEN:
    raise RuntimeError(
        "Set an HF_TOKEN secret. Accept the gate on "
        "https://huggingface.co/ideogram-ai/ideogram-4-nf4"
    )

log("startup: loading diffusers Ideogram4Pipeline nf4")
pipe = Ideogram4Pipeline.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,
    token=HF_TOKEN,
)
# Working ZeroGPU Spaces dequantize nf4 -> bf16 then move to emulated cuda
# at import. ZeroGPU packs those weights and streams them on first GPU call.
if hasattr(pipe, "transformer") and hasattr(pipe.transformer, "dequantize"):
    pipe.transformer.dequantize()
if hasattr(pipe, "unconditional_transformer") and hasattr(pipe.unconditional_transformer, "dequantize"):
    pipe.unconditional_transformer.dequantize()
pipe.to("cuda")

try:
    pipe.transformer.load_lora_adapter(
        LORA_REPO,
        weight_name=LORA_FILE,
        prefix="diffusion_model",
        token=HF_TOKEN,
    )
    log("startup: TurboTime LoRA loaded (2-8 steps, guidance_scale=1)")
except Exception as exc:
    log(f"startup: LoRA load failed, will run base CFG path: {exc!r}")

log("startup: pipeline on cuda (ZeroGPU-emulated until Generate)")


def get_duration(prompt, width, height, steps, progress=None):
    # Must accept the same args as generate(), including Gradio's progress.
    tokens = (int(width) // 16) * (int(height) // 16)
    seconds = 25 + int(steps) * max(0.35, 0.00035 * tokens)
    return int(min(max(seconds, 60), 180))


@spaces.GPU(duration=get_duration, size="xlarge")
def generate(prompt, width, height, steps, progress=gr.Progress(track_tqdm=True)):
    log(
        f"generate() cuda={torch.cuda.is_available()} "
        f"{int(width)}x{int(height)} steps={int(steps)}"
    )
    if not (prompt or "").strip():
        raise gr.Error("Prompt is empty.")
    if not torch.cuda.is_available():
        raise gr.Error("ZeroGPU did not attach CUDA to this call.")

    # Magic prompt OFF: feed the textbox verbatim (same as --no-magic-prompt).
    # This diffusers build defaults guidance_schedule to the 48-step CFG
    # curve. Passing guidance_scale at the same time raises:
    # "Only one of guidance_scale and guidance_schedule may be set."
    # TurboTime is tuned for a constant scale of 1.0 (no CFG).
    kwargs = dict(
        prompt=prompt,
        height=int(height),
        width=int(width),
        num_inference_steps=int(steps),
        guidance_scale=1.0,
        guidance_schedule=None,
        generator=torch.Generator(device="cuda").manual_seed(0),
    )
    # Some diffusers builds use prompt_upsampling=; keep it off if present.
    try:
        images = pipe(**kwargs, prompt_upsampling=False).images
    except TypeError:
        images = pipe(**kwargs).images
    image = images[0]
    if not isinstance(image, Image.Image):
        image = Image.fromarray(image)
    log(f"generate() ok {image.size}")
    return image


with gr.Blocks(title="Ideogram 4 (ZeroGPU)") as demo:
    gr.Markdown(
        "# Ideogram 4 on ZeroGPU\n"
        "This is the stack the working Spaces use: **diffusers + nf4 + "
        "TurboTime LoRA + `size='xlarge'`**. Magic prompt is **off** — "
        "your prompt is used as-is.\n\n"
        "Needs an `HF_TOKEN` whose account accepted "
        "[ideogram-ai/ideogram-4-nf4](https://huggingface.co/ideogram-ai/ideogram-4-nf4). "
        "First Space boot can take a long time while weights download."
    )
    with gr.Row():
        with gr.Column():
            prompt = gr.Textbox(
                label="Prompt",
                lines=3,
                placeholder="A photo of a cat holding a sign that says hello world",
            )
            with gr.Row():
                width = gr.Slider(256, 2048, value=1024, step=16, label="Width")
                height = gr.Slider(256, 2048, value=1024, step=16, label="Height")
            steps = gr.Slider(2, 12, value=8, step=1, label="Steps (TurboTime LoRA)")
            run_btn = gr.Button("Generate", variant="primary")
        with gr.Column():
            output = gr.Image(label="Result")
    run_btn.click(
        fn=generate,
        inputs=[prompt, width, height, steps],
        outputs=output,
        api_name="predict",
    )

demo.queue().launch(ssr_mode=False, show_error=True)