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)