multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
efb1aca verified
Raw
History Blame Contribute Delete
7.48 kB
"""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)