File size: 3,986 Bytes
10d0c77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3aa909c
 
 
 
10d0c77
 
 
 
 
3aa909c
10d0c77
 
 
 
 
 
 
 
 
 
 
3aa909c
 
 
10d0c77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os

import spaces
import torch
import gradio as gr
from transformers import AutoModelForImageTextToText, AutoProcessor

MODEL_ID = "CohereLabs/North-Micro-Vision-Instruct"

# Load once at startup. On ZeroGPU the weights stay resident and
# @spaces.GPU allocates a worker per call.
print(f"Loading {MODEL_ID} ...")
processor = AutoProcessor.from_pretrained(MODEL_ID)
model = AutoModelForImageTextToText.from_pretrained(
    MODEL_ID,
    dtype=torch.bfloat16,
    device_map="cuda",
)
print("Model loaded!")


def _estimate_duration(image, prompt, max_new_tokens, temperature, top_p, top_k) -> int:
    """Rough wall-clock estimate (seconds) for one VLM call. Requesting less
    than the 60s default raises queue priority and frees the GPU slot sooner
    for the next visitor. Scaled by max_new_tokens; clamped to a safe range."""
    seconds = 10 + int(max_new_tokens) * 0.15
    return max(20, min(int(seconds), 120))


def _friendly_gpu_error(err: Exception) -> str:
    msg = (str(err) or "").lower()
    capacity_hints = (
        "gpu limit", "reached its gpu limit", "gpu quota", "out of quota",
        "quota", "no gpu", "could not allocate", "gpu is busy", "too many",
        "concurrent",
    )
    if any(h in msg for h in capacity_hints):
        return (
            "⛔ This demo's shared GPU is at capacity right now — it's not a "
            "problem with your input or your account. Please wait a minute and retry."
        )
    if "out of memory" in msg or "oom" in msg:
        return (
            "💥 Ran out of GPU memory. Try a smaller image or fewer max new "
            "tokens, then retry."
        )
    return "⚠️ Generation failed. Please try again in a moment."


@spaces.GPU(duration=_estimate_duration)
def _run_vlm_gpu(image, prompt, max_new_tokens, temperature, top_p, top_k):
    """GPU worker: runs only under a ZeroGPU allocation."""
    if not prompt or not prompt.strip():
        raise gr.Error("Please enter a prompt.")
    if image is None:
        raise gr.Error("Please provide an image.")

    if isinstance(image, dict):
        image_ref = image.get("path") or image.get("url")
    else:
        image_ref = image

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "image", "url": image_ref},
                {"type": "text", "text": prompt},
            ],
        }
    ]

    inputs = processor.apply_chat_template(
        messages,
        tokenize=True,
        add_generation_prompt=True,
        return_tensors="pt",
        return_dict=True,
    ).to(model.device)
    if "pixel_values" in inputs:
        inputs["pixel_values"] = inputs["pixel_values"].to(torch.bfloat16)

    do_sample = float(temperature) > 0
    gen_kwargs = dict(max_new_tokens=int(max_new_tokens), do_sample=do_sample)
    if do_sample:
        gen_kwargs.update(
            temperature=float(temperature),
            top_p=float(top_p),
            top_k=int(top_k),
        )

    outputs = model.generate(**inputs, **gen_kwargs)
    generated_ids = outputs[0][inputs["input_ids"].shape[1]:]
    return processor.decode(
        generated_ids,
        skip_special_tokens=True,
        clean_up_tokenization_spaces=False,
    )


def run_vlm(image, prompt, max_new_tokens, temperature, top_p, top_k):
    """Workflow-facing wrapper bound to the canvas as a `fn` operator node.
    Catches ZeroGPU allocator rejections and rewords them for users."""
    try:
        return _run_vlm_gpu(image, prompt, max_new_tokens, temperature, top_p, top_k)
    except gr.Error:
        raise
    except Exception as e:
        raise gr.Error(_friendly_gpu_error(e)) from e


# The workflow (workflow.json) wires `run_vlm` as a `fn` operator:
#   Image, Prompt, Max New Tokens, Temperature, Top P, Top K ─▶
#   run_vlm (fn operator, kind="fn") ─▶ Response
demo = gr.Workflow(
    graph="workflow.json",
    bind={"run_vlm": run_vlm},
)

if __name__ == "__main__":
    demo.launch()