File size: 15,905 Bytes
a64255e 0ec5034 a64255e 0ec5034 a64255e | 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | """SIMIT demo: a vision-language model imagines its own practice examples for
your question, then answers again with them in context.
Runs on Hugging Face ZeroGPU (each request uses the visitor's own GPU quota)
and on any machine with a CUDA GPU (``python app.py``).
"""
import os
# ZeroGPU starts a fresh process per request: keep Triton's autotuning results on disk
# (otherwise every request re-tunes the FP8 / linear-attention kernels, ~20 s).
os.environ.setdefault("TRITON_CACHE_AUTOTUNING", "1")
os.environ.setdefault("TRANSFORMERS_DISABLE_DEEPGEMM_LINEAR", "1")
import spaces # before anything CUDA-related: ZeroGPU patches torch # noqa: E402 # isort: skip
import base64 # noqa: E402
import html # noqa: E402
import json # noqa: E402
from pathlib import Path # noqa: E402
import gradio as gr # noqa: E402
from PIL import Image # noqa: E402
from demo_models import BUDGETS, ENABLED, MODELS, code_snippet, run # noqa: E402
HERE = Path(__file__).parent
ASSETS = HERE / "assets"
EXAMPLES_DIR = HERE / "examples"
def _data_uri(path: Path) -> str:
return "data:image/webp;base64," + base64.b64encode(path.read_bytes()).decode()
MASCOT = {p.stem: _data_uri(p) for p in ASSETS.glob("*.webp")}
STAGES = { # stage -> (mascot, message)
"idle": ("initial", "Ask me anything about an image"),
"loading": ("initial", "Loading the model weights (first request only)"),
"warmup": ("initial", "Waking up the model on the GPU"),
"solving": ("initial", "Solving it on its own first"),
"synthesizing": ("synthesizer", "Writing practice questions it already knows the answers to"),
"routing": ("router", "Choosing how to draw each practice image"),
"drawing": ("artist", "Painting a practice image"),
"coding": ("architect", "Writing code for a chart, diagram or document"),
"checking": ("critic", "Checking that each picture matches its question"),
"improving": ("improved", "Answering again, with its own practice examples in context"),
}
LABELS = {MODELS[k].label: k for k in ENABLED}
BUDGET_LABELS = [f"{b} s" for b in BUDGETS]
GPU_OVERHEAD = 20 # seconds requested on top of the budget (worker start-up, final answer); weight transfer counts inside it
def _seconds(label: str) -> int:
return int(str(label).split()[0])
# --------------------------------------------------------------------- HTML
def status_html(stage: str, elapsed: float = 0.0, limit: float = 0.0, n_demos: int = 0, note: str = "") -> str:
mascot, message = STAGES.get(stage, STAGES["solving"])
dots = "" if stage == "idle" else '<span class="simit-dots"></span>'
bar = ""
if limit:
pct = max(2.0, min(100.0, 100.0 * elapsed / limit))
bar = (f'<div class="simit-bar"><div style="width:{pct:.0f}%"></div></div>'
f'<div class="simit-sub">{elapsed:.0f} s of {limit:.0f} s'
+ (f' · {n_demos} practice example{"s" if n_demos != 1 else ""} ready' if n_demos else "")
+ "</div>")
return (f'<div class="simit-status"><img class="simit-mascot" src="{MASCOT[mascot]}" alt="">'
f'<div><div class="simit-msg">{html.escape(message)}{dots}</div>'
f'{bar}{f"<div class=simit-sub>{html.escape(note)}</div>" if note else ""}</div></div>')
def done_html(note: str) -> str:
return (f'<div class="simit-status done"><img class="simit-mascot still" src="{MASCOT["improved"]}" alt="">'
f'<div><div class="simit-msg">Done</div><div class="simit-sub">{html.escape(note)}</div></div></div>')
def error_html(note: str) -> str:
return (f'<div class="simit-status error"><img class="simit-mascot still" src="{MASCOT["critic"]}" alt="">'
f'<div><div class="simit-msg">Something went wrong</div><div class="simit-sub">{html.escape(note)}'
'</div></div></div>')
def answer_card(kind: str, answer: str = "", sub: str = "", mark: str = "") -> str:
title = "Base model (greedy)" if kind == "base" else "SIMIT-ICL (self-improved)"
body = html.escape(answer) if answer else '<span class="simit-wait">…</span>'
size = " long" if len(answer) > 60 else ""
badge = {"right": '<span class="simit-ok">✓</span>', "wrong": '<span class="simit-no">✗</span>'}.get(mark, "")
return (f'<div class="simit-card {kind}"><div class="simit-card-title">{title}{badge}</div>'
f'<div class="simit-answer{size}">{body}</div><div class="simit-card-sub">{html.escape(sub)}</div></div>')
def demo_caption(d) -> str:
bits = [f"Q: {d.question}", f"A: {d.answer}"]
meta = [d.skill] if d.skill else []
if d.verify_score is not None:
meta.append(f"critic {d.verify_score}/100")
if d.confidence is not None:
meta.append(f"confidence {d.confidence:.2f}")
return "\n".join(bits) + (f"\n({', '.join(meta)})" if meta else "")
# ------------------------------------------------------------- GPU requests
def _duration(image, question, key, budget_label, always, *args, **kwargs):
return _seconds(budget_label) + GPU_OVERHEAD
def _session(image, question, key, budget_label, always):
"""Runs inside the GPU call: turns pipeline events into UI updates."""
spec, budget = MODELS[key], _seconds(budget_label)
try:
yield from _events(spec, image, question, budget, always)
except Exception as e: # show it in the page instead of a bare error toast
print(f"[demo] request failed: {type(e).__name__}: {e}", flush=True)
yield (error_html(f"{type(e).__name__}: {str(e)[:300]}"), answer_card("base"), answer_card("simit"), [], "")
def _events(spec, image, question, budget, always):
greedy, k_budget, demos, gallery = None, 0, [], []
base, improved = answer_card("base"), answer_card("simit")
status = status_html("warmup")
for event in run(spec, image, question, budget, always):
kind = event[0]
if kind == "stage":
_, stage, info = event
status = status_html(stage, info.get("elapsed", 0), info.get("limit", 0), len(demos))
elif kind == "greedy":
_, zs, k_budget = event
greedy = zs
sub = f"confidence p0 = {zs.confidence:.2f}" if zs.confidence is not None else ""
base = answer_card("base", zs.answer, sub)
elif kind == "demo":
demos.append(event[1])
gallery = [(d.image, demo_caption(d)) for d in demos]
else: # final
_, answer, info = event
reason = info.get("skipped")
if reason == "confident":
sub = "The model was already confident, so SIMIT kept its answer (adaptive budget: 0 examples)."
elif reason == "policy":
sub = ("On general questions, our tuning found no gain from imagination for this model at this "
"budget, so SIMIT keeps the base answer. Tick 'Imagine even when confident' to try it anyway.")
elif reason == "no_time":
sub = "Not enough time budget left to imagine examples; kept the base answer."
elif reason == "none_passed":
sub = "No imagined example passed the checks in time; kept the base answer."
else:
changed = "changed" if answer.strip() != greedy.answer.strip() else "kept"
sub = f"{changed} the answer after {info['demos']} imagined example{'s' if info['demos'] > 1 else ''}"
improved = answer_card("simit", answer, sub)
status = done_html(f"{info['elapsed']:.0f} s on the GPU")
yield status, base, improved, gallery, _details(greedy, k_budget, demos, spec, budget)
@spaces.GPU(duration=_duration, size="large")
def gpu_large(image, question, key, budget_label, always):
yield from _session(image, question, key, budget_label, always)
@spaces.GPU(duration=_duration, size="xlarge")
def gpu_xlarge(image, question, key, budget_label, always):
yield from _session(image, question, key, budget_label, always)
def _details(greedy, k_budget, demos, spec, budget) -> str:
if greedy is None:
return ""
p0 = f"{greedy.confidence:.2f}" if greedy.confidence is not None else "n/a"
return (f"**{spec.label}**, {budget} s budget. Zero-shot confidence p0 = {p0}, so the adaptive budget asked "
f"for **{k_budget}** example{'s' if k_budget != 1 else ''}; **{len(demos)}** passed the checks. "
"Each example is a question the model wrote, whose answer it decided first, with an image made "
"to fit that answer.")
def submit(image, question, model_label, budget_label, always):
if image is None:
raise gr.Error("Please upload an image.")
if not question or not question.strip():
raise gr.Error("Please type a question about the image.")
key = LABELS[model_label]
spec = MODELS[key]
image = image.convert("RGB")
if spec._weights is None: # main process, CPU: no GPU time is spent here
yield (status_html("loading"), answer_card("base"), answer_card("simit"), [], "")
spec.load()
fn = gpu_xlarge if spec.gpu_size == "xlarge" else gpu_large
yield from fn(image, question.strip(), key, budget_label, always)
# ------------------------------------------------------------------ cached
def _load_examples():
index = EXAMPLES_DIR / "index.json"
return json.loads(index.read_text()) if index.exists() else []
EXAMPLES = [e for e in _load_examples() if e["model"] in ENABLED]
def show_cached(image, question, model_label, budget_label, always=False):
"""A cached example: the stored outputs of a real run, shown without using the GPU."""
entry = next((e for e in EXAMPLES if e["question"] == question and MODELS[e["model"]].label == model_label
and f"{e['budget']} s" == budget_label and bool(e.get("always")) == bool(always)), None)
if entry is None:
return status_html("idle", note="Press Submit to run this example."), answer_card("base"), \
answer_card("simit"), [], ""
d = EXAMPLES_DIR / entry["id"]
mark = lambda ok: "right" if ok else "wrong" # noqa: E731
gt = f"reference answer: {entry['reference']}"
base = answer_card("base", entry["greedy"], f"confidence p0 = {entry['p0']:.2f} · {gt}", mark(entry["greedy_ok"]))
n = len(entry["demos"])
improved = answer_card("simit", entry["simit"], f"after {n} imagined example{'s' if n != 1 else ''} · {gt}",
mark(entry["simit_ok"]))
gallery = [(str(d / x["image"]), "\n".join([f"Q: {x['question']}", f"A: {x['answer']}",
f"({x['skill']}" + (f", critic {x['verify_score']}/100" if
x.get("verify_score") is not None else "") + ")"]))
for x in entry["demos"]]
note = (f"Cached result from a real run ({entry['elapsed']:.0f} s on an H100, {entry['budget']} s budget"
+ (", imagining even when confident" if entry.get("always") else "") + f"). Source: {entry['source']}.")
details = (f"**{MODELS[entry['model']].label}**, {entry['budget']} s budget. Zero-shot confidence "
f"p0 = {entry['p0']:.2f}; {n} imagined example{'s' if n != 1 else ''} passed the checks.")
return done_html(note), base, improved, gallery, details
# ---------------------------------------------------------------------- UI
CSS = (ASSETS / "style.css").read_text()
PAPER_URL = "https://monurcan.github.io/simit"
INTRO = f"""
<div class="simit-hero">
<div class="simit-hero-top">
<h1>SIMIT: models that imagine their own practice examples</h1>
<a class="simit-paper" href="{PAPER_URL}" target="_blank" rel="noopener">Check our paper ↗</a>
</div>
<p>Ask any question about an image. The model first answers on its own. Then, without any labels, it writes
similar practice questions whose answers it decides first, makes an image for each one, keeps only those it
can verify, and answers your question again with them in context.</p>
</div>
"""
with gr.Blocks(title="SIMIT demo") as demo:
gr.HTML(INTRO)
with gr.Row(equal_height=False):
with gr.Column(scale=5):
image = gr.Image(type="pil", label="Image", height=360)
question = gr.Textbox(label="Question", placeholder="e.g. In what country would you find this hat?",
lines=2)
model = gr.Dropdown(list(LABELS), value=next(iter(LABELS)), label="Model")
budget = gr.Radio(BUDGET_LABELS, value="60 s", label="Max GPU time per request",
info="More time lets the model imagine and check more practice examples.")
with gr.Accordion("Advanced", open=False):
always = gr.Checkbox(False, label="Imagine even when the model is already confident",
info="By default SIMIT skips imagination for confident answers.")
run_btn = gr.Button("Submit", variant="primary")
with gr.Column(scale=6):
status = gr.HTML(status_html("idle", note="Upload an image and type a question, or pick an example below."))
with gr.Row():
base_out = gr.HTML(answer_card("base"), min_width=260)
simit_out = gr.HTML(answer_card("simit"), min_width=260)
with gr.Accordion("See the imagined practice examples", open=False):
details = gr.Markdown()
gallery = gr.Gallery(columns=4, height=300, object_fit="contain", show_label=False)
with gr.Accordion("Use SIMIT in your own code", open=False):
code = gr.Code(code_snippet(MODELS[next(iter(LABELS.values()))], 60, ""), language="python",
interactive=False)
outputs = [status, base_out, simit_out, gallery, details]
# On ZeroGPU each request gets its own GPU process; run locally, requests share this process and GPU.
from spaces.config import Config as _SpacesConfig
run_btn.click(submit, [image, question, model, budget, always], outputs,
concurrency_limit=4 if _SpacesConfig.zero_gpu else 1)
def _code(model_label, budget_label, q, force):
return code_snippet(MODELS[LABELS[model_label]], _seconds(budget_label), q or "", force)
for comp in (model, budget, question, always):
comp.change(_code, [model, budget, question, always], code, queue=False)
if EXAMPLES:
gr.Markdown("### Examples\nCached results from real runs of this demo (click one; press Submit to "
"run it again live).")
examples = gr.Gallery(
[(str(EXAMPLES_DIR / e["id"] / e["query_file"]),
f"{e['question'].splitlines()[0]} ({MODELS[e['model']].label})") for e in EXAMPLES],
columns=4, height=300 * ((len(EXAMPLES) + 3) // 4) + 20, object_fit="cover", allow_preview=False,
show_label=False)
def pick(evt: gr.SelectData):
e = EXAMPLES[evt.index]
inputs = [Image.open(EXAMPLES_DIR / e["id"] / e["query_file"]), e["question"],
MODELS[e["model"]].label, f"{e['budget']} s", bool(e.get("always"))]
return (*inputs, *show_cached(*inputs))
examples.select(pick, None, [image, question, model, budget, always] + outputs)
gr.Markdown("Models: " + ", ".join(f"[{MODELS[k].label}](https://huggingface.co/{MODELS[k].repo})"
for k in ENABLED)
+ ". Each request uses your own ZeroGPU quota for the selected time budget.")
if os.environ.get("SIMIT_DEMO_PRELOAD", "1") == "1":
for key in ENABLED: # load every model on CPU before serving (main process, no CUDA)
MODELS[key].load()
if __name__ == "__main__":
demo.queue(max_size=32).launch(css=CSS, theme=gr.themes.Soft(primary_hue="orange", secondary_hue="amber"))
|