akhaliq HF Staff commited on
Commit
8d46cb2
·
verified ·
1 Parent(s): 723e507

Run Iris-3B in-process on ZeroGPU via @spaces.GPU fn nodes

Browse files
Files changed (1) hide show
  1. app.py +108 -9
app.py CHANGED
@@ -1,17 +1,77 @@
1
- """Gradio Workflow app for the Iris-3B Space (speridlabs/iris-3b).
2
 
3
- Two pipelines on one canvas, editable in the browser:
 
 
 
 
4
 
5
- 1. "Iris Text-to-Image": Prompt -> enhance_prompt (Python fn) -> Iris-3B /run
6
- -> Generated Image + Seed Used.
7
- 2. "Aspect Ratio Comparison": one Prompt fans out to two Iris-3B nodes at
8
- 1344x768 and 768x1344 (same seed) -> Wide + Tall subjects.
9
-
10
- The graph lives in workflow.json; the canvas autosaves edits back to it.
11
  """
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  import gradio as gr
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  QUALITY_SUFFIX = "highly detailed, sharp focus, professional quality"
16
 
17
 
@@ -25,7 +85,46 @@ def enhance_prompt(prompt: str) -> str:
25
  return f"{prompt}, {QUALITY_SUFFIX}"
26
 
27
 
28
- demo = gr.Workflow(graph="workflow.json", bind={"enhance_prompt": enhance_prompt})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  if __name__ == "__main__":
31
  demo.launch()
 
1
+ """Gradio Workflow app that runs Iris-3B in-process on ZeroGPU.
2
 
3
+ The canvas's Iris-3B nodes are `kind: "fn"` operators backed by the
4
+ `illustrate` function below: the 3B pixel-space DiT and its text encoder are
5
+ loaded once at module scope, and each call books its own GPU slot via
6
+ `@spaces.GPU` — instead of calling the remote speridlabs/iris-3b Space through
7
+ gradio_client.
8
 
9
+ The `iris3b` package is not on PyPI, so it is pulled from the
10
+ speridlabs/iris-3b Space repo at startup; the weights come from the
11
+ speridlabs/iris-3b model repo. Requires the Space hardware to be set to
12
+ ZeroGPU (Settings -> Hardware).
 
 
13
  """
14
 
15
+ import os
16
+ import random
17
+ import sys
18
+ import tempfile
19
+
20
+ # `spaces` has to be imported before anything initializes CUDA. It only exists
21
+ # on Hugging Face Spaces, so shim it for local runs.
22
+ try:
23
+ import spaces
24
+ except ImportError:
25
+
26
+ class _SpacesShim:
27
+ @staticmethod
28
+ def GPU(duration=60):
29
+ return lambda fn: fn
30
+
31
+ spaces = _SpacesShim()
32
+
33
+ import torch
34
+ from gradio_client import utils as client_utils
35
+ from huggingface_hub import snapshot_download
36
+ from PIL import Image
37
+
38
  import gradio as gr
39
 
40
+ MODEL_REPO = os.environ.get("IRIS_MODEL_REPO", "speridlabs/iris-3b")
41
+ CODE_REPO = "speridlabs/iris-3b" # Space repo holding the iris3b package
42
+
43
+ _src = snapshot_download(CODE_REPO, repo_type="space", allow_patterns=["iris3b/**"])
44
+ sys.path.insert(0, _src)
45
+
46
+ import iris3b.text # noqa: F401, E402 (registers encoders)
47
+ from iris3b.config import inference_config # noqa: E402
48
+ from iris3b.models.dit import IrisDiT # noqa: E402
49
+ from iris3b.registry import TEXT_ENCODERS # noqa: E402
50
+ from iris3b.sampling import generate, load_for_inference # noqa: E402
51
+
52
+ # On ZeroGPU, `spaces` forces torch.cuda.is_available() to True (CUDA emulation
53
+ # at import, real CUDA inside @spaces.GPU), so this resolves to cuda there and
54
+ # to cpu when run locally.
55
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
56
+
57
+ # (height, width) buckets with real training mass, from the original demo.
58
+ SIZES = {f"{w}×{h}": (h, w) for h, w in [(768, 1344), (832, 1280), (896, 1152), (1024, 1024),
59
+ (1152, 896), (1280, 832), (1344, 768)]}
60
+
61
+ # Load once at import and move to the device here, not inside the function: on
62
+ # ZeroGPU the move is intercepted and the weights are transferred when a GPU is
63
+ # actually allocated, so module scope costs no GPU time.
64
+ source = MODEL_REPO if os.path.isdir(MODEL_REPO) else snapshot_download(
65
+ MODEL_REPO, token=os.environ.get("HF_TOKEN")
66
+ )
67
+ raw, weights = load_for_inference(source)
68
+ cfg = inference_config(raw, [])
69
+ with torch.device("meta"):
70
+ model = IrisDiT(cfg.model)
71
+ model.load_state_dict(weights, strict=True, assign=True)
72
+ model = model.eval().to(device=DEVICE, dtype=torch.float32) # FP32 weights, BF16 autocast at inference
73
+ text_encoder = TEXT_ENCODERS.build(cfg.text_encoder.name, cfg.text_encoder, device=DEVICE)
74
+
75
  QUALITY_SUFFIX = "highly detailed, sharp focus, professional quality"
76
 
77
 
 
85
  return f"{prompt}, {QUALITY_SUFFIX}"
86
 
87
 
88
+ # duration is per node, not per pipeline: each illustrate call books and
89
+ # releases its own GPU slot. ZeroGPU multiplies this by the hardware factor
90
+ # (1.5 on RTX PRO 6000 Blackwell), so 120s books 180s against the caller.
91
+ @spaces.GPU(duration=120)
92
+ def illustrate(prompt: str, negative: str, size: str, steps: float,
93
+ cfg_scale: float, seed: float, randomize: bool):
94
+ """Generate one image with Iris-3B. Returns (image file dict, seed)."""
95
+ if not (prompt or "").strip():
96
+ raise gr.Error("Connect a prompt to this node.")
97
+ if randomize:
98
+ seed = random.randint(0, 2**31 - 1)
99
+ if size not in SIZES:
100
+ size = "1024×1024"
101
+ height, width = SIZES[size]
102
+ with torch.autocast(DEVICE.type, dtype=torch.bfloat16, enabled=DEVICE.type == "cuda"):
103
+ image = generate(
104
+ model, text_encoder, [prompt], height=height, width=width, steps=int(steps),
105
+ order=cfg.sample.order, cfg_scale=cfg_scale,
106
+ cfg_interval=tuple(cfg.sample.cfg_interval), shift=cfg.flow.shift,
107
+ negative_prompt=negative or "",
108
+ generator=torch.Generator(device=DEVICE).manual_seed(int(seed)), device=DEVICE,
109
+ num_train_timesteps=cfg.flow.num_train_timesteps, prediction=cfg.flow.prediction,
110
+ )[0]
111
+ pixels = ((image.float().clamp(-1, 1) + 1) * 127.5).round().byte().permute(1, 2, 0).cpu().numpy()
112
+ path = os.path.join(tempfile.gettempdir(), f"iris_{os.urandom(8).hex()}.png")
113
+ Image.fromarray(pixels).save(path)
114
+ # The canvas renders media only from a dict carrying a url; launch()
115
+ # already allowlists the system tempdir.
116
+ media = {
117
+ "path": path,
118
+ "url": f"/gradio_api/file={client_utils.encode_file_path(path)}",
119
+ "is_file": True,
120
+ }
121
+ return media, int(seed)
122
+
123
+
124
+ demo = gr.Workflow(
125
+ graph="workflow.json",
126
+ bind={"enhance_prompt": enhance_prompt, "illustrate": illustrate},
127
+ )
128
 
129
  if __name__ == "__main__":
130
  demo.launch()