akhaliq HF Staff commited on
Commit
10d0c77
·
1 Parent(s): 3493500

Add ZeroGPU gr.Workflow app for North-Micro-Vision-Instruct

Browse files
Files changed (5) hide show
  1. README.md +1 -1
  2. app.py +133 -0
  3. requirements.txt +7 -0
  4. run.py +0 -6
  5. workflow.json +137 -1
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.22.0
8
- app_file: run.py
9
  pinned: false
10
  hf_oauth: true
11
  ---
 
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.22.0
8
+ app_file: app.py
9
  pinned: false
10
  hf_oauth: true
11
  ---
app.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import spaces
4
+ import torch
5
+ import gradio as gr
6
+ from transformers import AutoModelForImageTextToText, AutoProcessor
7
+
8
+ MODEL_ID = "CohereLabs/North-Micro-Vision-Instruct"
9
+
10
+ # Load once at startup. On ZeroGPU the weights stay resident and
11
+ # @spaces.GPU allocates a worker per call.
12
+ print(f"Loading {MODEL_ID} ...")
13
+ processor = AutoProcessor.from_pretrained(MODEL_ID)
14
+ model = AutoModelForImageTextToText.from_pretrained(
15
+ MODEL_ID,
16
+ dtype=torch.bfloat16,
17
+ device_map="cuda",
18
+ )
19
+ print("Model loaded!")
20
+
21
+
22
+ def _load_image(image):
23
+ """Accept a workflow image value (dict with 'path', or a path/URL string)
24
+ and return a PIL.Image."""
25
+ from PIL import Image
26
+
27
+ if isinstance(image, dict):
28
+ image = image.get("path") or image.get("url")
29
+ if isinstance(image, str) and image.startswith(("http://", "https://")):
30
+ import requests
31
+ from io import BytesIO
32
+
33
+ return Image.open(BytesIO(requests.get(image, timeout=30).content)).convert("RGB")
34
+ return Image.open(image).convert("RGB")
35
+
36
+
37
+ def _estimate_duration(image, prompt, max_new_tokens, temperature, top_p, top_k) -> int:
38
+ """Rough wall-clock estimate (seconds) for one VLM call. Requesting less
39
+ than the 60s default raises queue priority and frees the GPU slot sooner
40
+ for the next visitor. Scaled by max_new_tokens; clamped to a safe range."""
41
+ seconds = 10 + int(max_new_tokens) * 0.15
42
+ return max(20, min(int(seconds), 120))
43
+
44
+
45
+ def _friendly_gpu_error(err: Exception) -> str:
46
+ msg = (str(err) or "").lower()
47
+ capacity_hints = (
48
+ "gpu limit", "reached its gpu limit", "gpu quota", "out of quota",
49
+ "quota", "no gpu", "could not allocate", "gpu is busy", "too many",
50
+ "concurrent",
51
+ )
52
+ if any(h in msg for h in capacity_hints):
53
+ return (
54
+ "⛔ This demo's shared GPU is at capacity right now — it's not a "
55
+ "problem with your input or your account. Please wait a minute and retry."
56
+ )
57
+ if "out of memory" in msg or "oom" in msg:
58
+ return (
59
+ "💥 Ran out of GPU memory. Try a smaller image or fewer max new "
60
+ "tokens, then retry."
61
+ )
62
+ return "⚠️ Generation failed. Please try again in a moment."
63
+
64
+
65
+ @spaces.GPU(duration=_estimate_duration)
66
+ def _run_vlm_gpu(image, prompt, max_new_tokens, temperature, top_p, top_k):
67
+ """GPU worker: runs only under a ZeroGPU allocation."""
68
+ if not prompt or not prompt.strip():
69
+ raise gr.Error("Please enter a prompt.")
70
+ if image is None:
71
+ raise gr.Error("Please provide an image.")
72
+
73
+ pil_image = _load_image(image)
74
+
75
+ messages = [
76
+ {
77
+ "role": "user",
78
+ "content": [
79
+ {"type": "image"},
80
+ {"type": "text", "text": prompt},
81
+ ],
82
+ }
83
+ ]
84
+
85
+ inputs = processor.apply_chat_template(
86
+ messages,
87
+ tokenize=True,
88
+ add_generation_prompt=True,
89
+ return_tensors="pt",
90
+ return_dict=True,
91
+ )
92
+ inputs = {k: v.to(model.device) if hasattr(v, "to") else v for k, v in inputs.items()}
93
+ inputs["pixel_values"] = inputs["pixel_values"].to(torch.bfloat16)
94
+
95
+ do_sample = float(temperature) > 0
96
+ gen_kwargs = dict(max_new_tokens=int(max_new_tokens), do_sample=do_sample)
97
+ if do_sample:
98
+ gen_kwargs.update(
99
+ temperature=float(temperature),
100
+ top_p=float(top_p),
101
+ top_k=int(top_k),
102
+ )
103
+
104
+ outputs = model.generate(**inputs, **gen_kwargs)
105
+ generated_ids = outputs[0][inputs["input_ids"].shape[1]:]
106
+ return processor.decode(
107
+ generated_ids,
108
+ skip_special_tokens=True,
109
+ clean_up_tokenization_spaces=False,
110
+ )
111
+
112
+
113
+ def run_vlm(image, prompt, max_new_tokens, temperature, top_p, top_k):
114
+ """Workflow-facing wrapper bound to the canvas as a `fn` operator node.
115
+ Catches ZeroGPU allocator rejections and rewords them for users."""
116
+ try:
117
+ return _run_vlm_gpu(image, prompt, max_new_tokens, temperature, top_p, top_k)
118
+ except gr.Error:
119
+ raise
120
+ except Exception as e:
121
+ raise gr.Error(_friendly_gpu_error(e)) from e
122
+
123
+
124
+ # The workflow (workflow.json) wires `run_vlm` as a `fn` operator:
125
+ # Image, Prompt, Max New Tokens, Temperature, Top P, Top K ─▶
126
+ # run_vlm (fn operator, kind="fn") ─▶ Response
127
+ demo = gr.Workflow(
128
+ graph="workflow.json",
129
+ bind={"run_vlm": run_vlm},
130
+ )
131
+
132
+ if __name__ == "__main__":
133
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch
2
+ accelerate
3
+ pillow
4
+ requests
5
+ spaces
6
+ gradio
7
+ git+https://github.com/huggingface/transformers.git
run.py DELETED
@@ -1,6 +0,0 @@
1
- import gradio as gr
2
-
3
- demo = gr.Workflow()
4
-
5
- if __name__ == "__main__":
6
- demo.launch()
 
 
 
 
 
 
 
workflow.json CHANGED
@@ -1 +1,137 @@
1
- {"schema_version":"2","name":"My Workflow","runtime":{"default":"client"},"references":[],"operators":[],"subjects":[],"edges":[],"view":{"default":"canvas"}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": "2",
3
+ "name": "North Micro Vision Instruct",
4
+ "description": "Visual question answering, captioning, OCR, and grounding with CohereLabs/North-Micro-Vision-Instruct (2.4B native-resolution VLM) on ZeroGPU, driven by a gr.Workflow fn-bound @spaces.GPU function. Single Space: the workflow frontend and the GPU worker share one process.",
5
+ "runtime": { "default": "client" },
6
+ "view": { "default": "canvas" },
7
+ "references": [
8
+ {
9
+ "id": "ref_image",
10
+ "label": "Input Image",
11
+ "role": "reference",
12
+ "asset_type": "image",
13
+ "inputs": [{ "id": "in", "label": "Image", "type": "image" }],
14
+ "outputs": [{ "id": "out", "label": "Image", "type": "image" }],
15
+ "x": 60,
16
+ "y": 120,
17
+ "width": 240,
18
+ "height": 140,
19
+ "data": {}
20
+ },
21
+ {
22
+ "id": "ref_prompt",
23
+ "label": "Prompt",
24
+ "role": "reference",
25
+ "asset_type": "text",
26
+ "inputs": [{ "id": "in", "label": "Prompt", "type": "text" }],
27
+ "outputs": [{ "id": "out", "label": "Prompt", "type": "text" }],
28
+ "x": 60,
29
+ "y": 300,
30
+ "width": 240,
31
+ "height": 120,
32
+ "data": { "out": "What do you see in this image?" }
33
+ },
34
+ {
35
+ "id": "ref_max_tokens",
36
+ "label": "Max New Tokens",
37
+ "role": "reference",
38
+ "asset_type": "number",
39
+ "inputs": [{ "id": "in", "label": "Max New Tokens", "type": "number" }],
40
+ "outputs": [{ "id": "out", "label": "Max New Tokens", "type": "number" }],
41
+ "x": 60,
42
+ "y": 460,
43
+ "width": 200,
44
+ "height": 90,
45
+ "data": { "out": 256 }
46
+ },
47
+ {
48
+ "id": "ref_temperature",
49
+ "label": "Temperature",
50
+ "role": "reference",
51
+ "asset_type": "number",
52
+ "inputs": [{ "id": "in", "label": "Temperature", "type": "number" }],
53
+ "outputs": [{ "id": "out", "label": "Temperature", "type": "number" }],
54
+ "x": 60,
55
+ "y": 570,
56
+ "width": 200,
57
+ "height": 90,
58
+ "data": { "out": 0.7 }
59
+ },
60
+ {
61
+ "id": "ref_top_p",
62
+ "label": "Top P",
63
+ "role": "reference",
64
+ "asset_type": "number",
65
+ "inputs": [{ "id": "in", "label": "Top P", "type": "number" }],
66
+ "outputs": [{ "id": "out", "label": "Top P", "type": "number" }],
67
+ "x": 60,
68
+ "y": 680,
69
+ "width": 200,
70
+ "height": 90,
71
+ "data": { "out": 0.8 }
72
+ },
73
+ {
74
+ "id": "ref_top_k",
75
+ "label": "Top K",
76
+ "role": "reference",
77
+ "asset_type": "number",
78
+ "inputs": [{ "id": "in", "label": "Top K", "type": "number" }],
79
+ "outputs": [{ "id": "out", "label": "Top K", "type": "number" }],
80
+ "x": 60,
81
+ "y": 790,
82
+ "width": 200,
83
+ "height": 90,
84
+ "data": { "out": 20 }
85
+ }
86
+ ],
87
+ "operators": [
88
+ {
89
+ "id": "op_vlm",
90
+ "label": "run_vlm",
91
+ "role": "operator",
92
+ "kind": "fn",
93
+ "source": "fn",
94
+ "fn": "run_vlm",
95
+ "inputs": [
96
+ { "id": "in_0", "label": "image", "type": "image", "required": true },
97
+ { "id": "in_1", "label": "prompt", "type": "text", "required": true },
98
+ { "id": "in_2", "label": "max_new_tokens", "type": "number" },
99
+ { "id": "in_3", "label": "temperature", "type": "number" },
100
+ { "id": "in_4", "label": "top_p", "type": "number" },
101
+ { "id": "in_5", "label": "top_k", "type": "number" }
102
+ ],
103
+ "outputs": [
104
+ { "id": "out_0", "label": "response", "type": "text", "output_index": 0 }
105
+ ],
106
+ "x": 420,
107
+ "y": 380,
108
+ "width": 280,
109
+ "height": 220,
110
+ "data": {}
111
+ }
112
+ ],
113
+ "subjects": [
114
+ {
115
+ "id": "sub_response",
116
+ "label": "Response",
117
+ "role": "subject",
118
+ "asset_type": "text",
119
+ "inputs": [{ "id": "in", "label": "Response", "type": "text" }],
120
+ "outputs": [{ "id": "out", "label": "Response", "type": "text" }],
121
+ "x": 820,
122
+ "y": 380,
123
+ "width": 280,
124
+ "height": 140,
125
+ "data": {}
126
+ }
127
+ ],
128
+ "edges": [
129
+ { "id": "e_image", "from_node_id": "ref_image", "from_port_id": "out", "to_node_id": "op_vlm", "to_port_id": "in_0", "type": "image" },
130
+ { "id": "e_prompt", "from_node_id": "ref_prompt", "from_port_id": "out", "to_node_id": "op_vlm", "to_port_id": "in_1", "type": "text" },
131
+ { "id": "e_max_tokens", "from_node_id": "ref_max_tokens", "from_port_id": "out", "to_node_id": "op_vlm", "to_port_id": "in_2", "type": "number" },
132
+ { "id": "e_temperature", "from_node_id": "ref_temperature", "from_port_id": "out", "to_node_id": "op_vlm", "to_port_id": "in_3", "type": "number" },
133
+ { "id": "e_top_p", "from_node_id": "ref_top_p", "from_port_id": "out", "to_node_id": "op_vlm", "to_port_id": "in_4", "type": "number" },
134
+ { "id": "e_top_k", "from_node_id": "ref_top_k", "from_port_id": "out", "to_node_id": "op_vlm", "to_port_id": "in_5", "type": "number" },
135
+ { "id": "e_response", "from_node_id": "op_vlm", "from_port_id": "out_0", "to_node_id": "sub_response", "to_port_id": "in", "type": "text" }
136
+ ]
137
+ }