Spaces:
Running on Zero
Running on Zero
File size: 7,476 Bytes
15f94fc efb1aca 15f94fc efb1aca 15f94fc 6105bdb 15f94fc efb1aca 15f94fc 6105bdb 15f94fc 6105bdb | 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 | """CVRR — Causal Visual Recurrent Reasoning, on Qwen2.5-VL-7B.
Paper: "Reason Through the Latent! Making Latent Visual Reasoning Necessary"
(arXiv:2609.06746, Park, Jung & Kang)
Code: https://github.com/dmis-lab/CVRR
"""
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # noqa: E402 (must precede torch / CUDA-touching imports)
import time # noqa: E402
import torch # noqa: E402
import gradio as gr # noqa: E402
import transformers # noqa: E402
from transformers import AutoModelForImageTextToText, AutoProcessor # noqa: E402
MODEL_ID = "dmis-lab/Qwen2.5-VL-7B-CVRR"
# --------------------------------------------------------------------------
# The released CVRR code re-instantiates the processor on every
# `prepare_inputs` call. Cache it so the cost is paid once, at import time,
# instead of inside every GPU window.
# --------------------------------------------------------------------------
_PROCESSORS: dict = {}
_orig_processor_from_pretrained = AutoProcessor.from_pretrained
def _cached_processor_from_pretrained(path, **kwargs):
key = str(path)
if key not in _PROCESSORS:
_PROCESSORS[key] = _orig_processor_from_pretrained(path, **kwargs)
return _PROCESSORS[key]
transformers.AutoProcessor.from_pretrained = _cached_processor_from_pretrained
print(f"Loading {MODEL_ID} ...", flush=True)
model = AutoModelForImageTextToText.from_pretrained(
MODEL_ID,
trust_remote_code=True,
dtype=torch.bfloat16,
device_map="cpu", # ZeroGPU: build on CPU, then hand the whole replica to .to("cuda")
).eval()
model = model.to("cuda")
RELEASE = model.config.release
NATIVE_DIR = model.release_path / "native_backbone"
processor = _cached_processor_from_pretrained(
NATIVE_DIR, local_files_only=True, use_fast=False
)
tokenizer = processor.tokenizer
model.tokenizer = tokenizer
EOS_TOKEN_ID = tokenizer.eos_token_id
print("Model ready.", flush=True)
def _estimate_duration(
image=None, question=None, max_new_tokens=64, max_visual_tokens=4096, *args, **kwargs
) -> int:
"""ZeroGPU reservation: cold-start weight streaming plus input-dependent decode."""
try:
tokens = int(max_new_tokens)
visual = int(max_visual_tokens)
except (TypeError, ValueError):
tokens, visual = 64, 4096
return int(min(75, 22 + tokens * 0.045 + visual * 0.0015))
@spaces.GPU(duration=_estimate_duration)
def answer_question(
image,
question: str,
max_new_tokens: int = 64,
max_visual_tokens: int = 4096,
progress=gr.Progress(track_tqdm=True),
) -> tuple[str, str]:
"""Answer a question about an image with CVRR latent visual recurrent reasoning.
Args:
image: the input image (PIL image) to reason about.
question: a question about the image, in natural language.
max_new_tokens: maximum number of answer tokens to decode greedily.
max_visual_tokens: ceiling on visual tokens fed to the recurrent transition.
Returns:
A tuple of (answer text, a short run-info line).
"""
if image is None:
raise gr.Error("Please provide an image.")
question = (question or "").strip()
if not question:
raise gr.Error("Please type a question about the image.")
image = image.convert("RGB")
started = time.perf_counter()
inputs = model.prepare_inputs(
image, question, max_visual_tokens=int(max_visual_tokens)
)
n_visual = int(inputs["input_ids"].eq(model.runtime.config.image_token_id).sum())
token_ids = model.generate(
**inputs,
do_sample=False,
max_new_tokens=int(max_new_tokens),
eos_token_id=EOS_TOKEN_ID,
)
answer = tokenizer.decode(token_ids[0], skip_special_tokens=True).strip()
elapsed = time.perf_counter() - started
info = (
f"**{n_visual}** visual tokens · **{int(RELEASE['inference_T'])}** recurrent steps "
f"(shared decoder layer {int(RELEASE['recurrent_layer'])}, boundary "
f"ℓ\\*={int(RELEASE['ell_star'])}) · **{elapsed:.1f} s**"
)
return (answer or "(empty answer)"), info
DESCRIPTION = """
# 🔁 CVRR — Reason Through the Latent
**Causal Visual Recurrent Reasoning** on top of `Qwen2.5-VL-7B-Instruct`. The lower
decoder is frozen up to the causal visual-read boundary; a single decoder layer is then
reused as a **shared recurrent transition** that refines the question state while keeping
the native visual rows as fixed evidence. Only the final recurrent state reaches the
answer decoder — the original visual rows and multimodal prefix cache are cut off, which
makes the latent visual reasoning *causally necessary* rather than merely present.
Ask a question about an image — open-ended, or in the benchmark-style multiple-choice
format the model was tuned on. Decoding is deterministic (greedy) by design. The recurrent
transition is supervised on short VQA answers, so crisp, targeted questions work best;
long free-form descriptions drift.
[Paper](https://huggingface.co/papers/2609.06746) · [Code](https://github.com/dmis-lab/CVRR) · [Model](https://huggingface.co/dmis-lab/Qwen2.5-VL-7B-CVRR)
"""
CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
EXAMPLES = [
["examples/mmvp_11.jpg", "Has the peacock opened its tail? Answer briefly."],
["examples/mmvp_7.jpg", "Is the school bus driving towards or away from the camera?"],
["examples/mmvp_1.jpg", "Are the butterfly's wings closer to being open or closed?"],
[
"examples/mmvp_5.jpg",
"Is the dog facing left or right from the camera's perspective?\n"
"(a) Left (b) Right\nAnswer with only the letter of the correct option.",
],
]
with gr.Blocks() as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown(DESCRIPTION)
with gr.Row():
with gr.Column():
image_in = gr.Image(type="pil", label="Image", height=360)
with gr.Column():
question_in = gr.Textbox(
label="Question",
lines=3,
placeholder="Is the peacock's tail open or closed?",
)
run_btn = gr.Button("Answer", variant="primary")
answer_out = gr.Textbox(label="Answer", lines=4)
info_out = gr.Markdown()
with gr.Accordion("Advanced settings", open=False):
max_new_tokens_in = gr.Slider(
minimum=1, maximum=256, value=64, step=1, label="Max new tokens"
)
max_visual_tokens_in = gr.Slider(
minimum=256,
maximum=8192,
value=4096,
step=256,
label="Max visual tokens (image resolution budget)",
)
gr.Examples(
examples=EXAMPLES,
inputs=[image_in, question_in],
outputs=[answer_out, info_out],
fn=answer_question,
cache_examples=True,
cache_mode="lazy",
)
gr.on(
triggers=[run_btn.click, question_in.submit],
fn=answer_question,
inputs=[image_in, question_in, max_new_tokens_in, max_visual_tokens_in],
outputs=[answer_out, info_out],
api_name="answer_question",
)
if __name__ == "__main__":
demo.queue().launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)
|