Spaces:
Running on Zero
Running on Zero
| """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)) | |
| 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) | |