multimodalart HF Staff commited on
Commit
93161e4
·
verified ·
1 Parent(s): ccfbf24

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +16 -15
  2. app.py +210 -173
  3. examples/astronaut.jpg +0 -0
  4. examples/bird_bee_eater.jpg +0 -0
  5. requirements.txt +5 -5
README.md CHANGED
@@ -1,30 +1,31 @@
1
  ---
2
- title: Grug-9B Reasoning VLM
3
- emoji: 🧠
4
  colorFrom: red
5
  colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
  app_file: app.py
9
- short_description: 9B reasoning VLM with vision and shortened thinking
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
  ---
13
 
14
- # Grug-9B Reasoning VLM
15
 
16
- A demo for [ProCreations/grug-9b](https://huggingface.co/ProCreations/grug-9b) — a 9B-parameter vision-language reasoning model fine-tuned from Ornith-1.0-9B (Qwen3.5 architecture) to produce shorter internal reasoning while maintaining coding and agent capabilities.
 
 
17
 
18
  ## Features
19
 
20
- - **Vision + Language**: Upload an image and ask questions about it.
21
- - **Reasoning**: The model thinks inside `` tags before producing its answer.
22
- - **Chat**: Multi-turn conversation with the model.
23
- - **Code**: Ask the model to write functions, explain concepts, etc.
24
 
25
- ## Usage
26
 
27
- 1. Optionally upload an image.
28
- 2. Type your message in the text box.
29
- 3. Press Send or hit Enter.
30
- 4. Adjust advanced settings (max tokens, temperature, thinking mode) as needed.
 
1
  ---
2
+ title: Grug-9B Demo
3
+ emoji: 🪨
4
  colorFrom: red
5
  colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 6.15.1
8
  app_file: app.py
9
+ short_description: Vision-language reasoning model with concise thinking
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
  ---
13
 
14
+ # 🪨 Grug-9B Vision-Language Demo
15
 
16
+ A Gradio demo for [ProCreations/grug-9b](https://huggingface.co/ProCreations/grug-9b) —
17
+ a 9B-parameter reasoning VLM fine-tuned from Ornith-1.0-9B (Qwen3.5 architecture)
18
+ that produces concise reasoning traces ("grug think small") instead of verbose chain-of-thought.
19
 
20
  ## Features
21
 
22
+ - **Image + text input**: Upload an image and ask questions about it
23
+ - **Thinking mode**: Toggle the model's reasoning traces (default on)
24
+ - **Adjustable generation**: Max tokens, temperature, top-p sampling
 
25
 
26
+ ## Model details
27
 
28
+ - Architecture: Qwen3.5 (`Qwen3_5ForConditionalGeneration`)
29
+ - Parameters: ~9.4B (dense, bf16)
30
+ - Base model: [deepreinforce-ai/Ornith-1.0-9B](https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B)
31
+ - License: MIT
app.py CHANGED
@@ -2,128 +2,179 @@ import os
2
 
3
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
4
 
5
- import spaces # MUST come before torch / transformers
6
  import torch
7
  import gradio as gr
8
- import numpy as np
9
- from PIL import Image
10
- from threading import Thread
11
- from transformers import (
12
- AutoProcessor,
13
- Qwen3_5ForConditionalGeneration,
14
- TextIteratorStreamer,
15
- )
16
 
17
  MODEL_ID = "ProCreations/grug-9b"
 
 
 
 
18
 
19
- print(f"Loading model {MODEL_ID} ...")
20
- processor = AutoProcessor.from_pretrained(MODEL_ID)
21
- model = Qwen3_5ForConditionalGeneration.from_pretrained(
22
  MODEL_ID,
23
- torch_dtype=torch.bfloat16,
24
- ).to("cuda").eval()
 
 
25
  print("Model loaded.")
26
 
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  @spaces.GPU(duration=120)
29
- def chat_with_image(
30
- image: Image.Image,
31
- message: str,
32
- history: list,
33
- max_new_tokens: int = 1024,
34
- temperature: float = 0.7,
35
- enable_thinking: bool = True,
36
- ):
37
- """Chat with the grug-9b reasoning VLM about an image.
38
 
39
  Args:
40
- image: An optional image to discuss with the model.
41
- message: The user's text message / question.
42
- history: Prior conversation turns (list of [user, assistant] pairs).
43
- max_new_tokens: Maximum number of tokens to generate.
44
- temperature: Sampling temperature; lower = more deterministic.
45
- enable_thinking: Whether the model produces reasoning inside tags.
 
 
 
 
46
  """
47
- if not message or not message.strip():
48
- yield history + [(message, "")], "Please enter a message."
49
- return
50
 
51
- # Build the conversation
52
  messages = []
 
 
 
 
 
 
 
 
 
 
 
 
53
  if image is not None:
54
- messages.append({
55
- "role": "user",
56
- "content": [
57
- {"type": "image", "image": image},
58
- {"type": "text", "text": message},
59
- ],
60
- })
61
- else:
62
- messages.append({"role": "user", "content": message})
63
-
64
- # Add conversation history
65
- conv_messages = []
66
- for user_msg, asst_msg in history:
67
- conv_messages.append({"role": "user", "content": user_msg})
68
- if asst_msg:
69
- conv_messages.append({"role": "assistant", "content": asst_msg})
70
- conv_messages.extend(messages)
71
-
72
- # Prepare inputs
73
- if image is not None:
74
- text = processor.apply_chat_template(
75
- conv_messages,
76
- tokenize=False,
77
- add_generation_prompt=True,
78
- enable_thinking=enable_thinking,
79
- )
80
- inputs = processor(
81
- text=[text],
82
- images=[image],
83
- return_tensors="pt",
84
- padding=True,
85
- ).to("cuda")
86
- else:
87
- text = processor.apply_chat_template(
88
- conv_messages,
89
- tokenize=False,
90
- add_generation_prompt=True,
91
- enable_thinking=enable_thinking,
92
- )
93
- inputs = processor(
94
- text=[text],
95
- return_tensors="pt",
96
- padding=True,
97
- ).to("cuda")
98
-
99
- # Stream the response
100
- streamer = TextIteratorStreamer(
101
- processor.tokenizer,
102
- skip_prompt=True,
103
- skip_special_tokens=True,
104
- timeout=120,
105
- )
106
 
107
- generation_kwargs = dict(
108
- **inputs,
109
- streamer=streamer,
110
- max_new_tokens=max_new_tokens,
111
- temperature=temperature,
112
- use_cache=True,
113
  )
114
 
115
- thread = Thread(target=model.generate, kwargs=generation_kwargs)
116
- thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- full_response = ""
119
- new_history = history + [(message, "")]
120
- for token in streamer:
121
- full_response += token
122
- new_history[-1] = (message, full_response)
123
- yield new_history, ""
 
 
 
124
 
125
- thread.join()
126
- yield new_history, ""
 
127
 
128
 
129
  CSS = """
@@ -134,86 +185,72 @@ CSS = """
134
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
135
  gr.Markdown(
136
  """
137
- # 🧠 Grug-9B Reasoning VLM
138
- A 9B-parameter vision-language reasoning model fine-tuned from Ornith-1.0-9B (Qwen3.5 architecture)
139
- to "think small" shorter internal reasoning while maintaining coding and agent capabilities.
 
 
 
 
140
 
141
- Upload an image (optional) and ask a question. The model reasons inside `` tags before answering.
142
  """
143
  )
144
 
145
- with gr.Column(elem_id="col-container"):
146
- with gr.Row():
147
  image_input = gr.Image(
148
- label="Image (optional)",
149
- type="pil",
150
- height=350,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  )
152
- with gr.Column(scale=2):
153
- chatbot = gr.Chatbot(
154
- label="Chat",
155
- height=450,
156
- show_label=True,
157
- )
158
- msg_input = gr.Textbox(
159
- label="Message",
160
- placeholder="Ask something about the image, or just chat...",
161
- show_label=False,
162
- lines=2,
163
- )
164
- with gr.Row():
165
- send_btn = gr.Button("Send", variant="primary", scale=2)
166
- clear_btn = gr.Button("Clear", scale=1)
167
-
168
- with gr.Accordion("Advanced settings", open=False):
169
- with gr.Row():
170
- max_tokens = gr.Slider(
171
- label="Max new tokens",
172
- minimum=128,
173
- maximum=4096,
174
- value=1024,
175
- step=128,
176
- )
177
- temperature = gr.Slider(
178
- label="Temperature",
179
- minimum=0.1,
180
- maximum=2.0,
181
- value=0.7,
182
- step=0.1,
183
- )
184
- thinking_toggle = gr.Checkbox(
185
- label="Enable thinking (reasoning mode)",
186
- value=True,
187
- )
188
-
189
- gr.Examples(
190
- examples=[
191
- [None, "Write a Python function that checks if a number is prime.", 512, 0.7, True],
192
- [None, "Explain the difference between TCP and UDP in networking.", 1024, 0.7, True],
193
- ["examples/cat_tabby.jpg", "What breed is this cat? Describe what you see in detail.", 1024, 0.7, True],
194
- ["examples/city_skyline_night.jpg", "What city might this be? Describe the architectural style.", 1024, 0.7, True],
195
- ["examples/sushi_platter.jpg", "What kinds of sushi are on this platter?", 1024, 0.7, True],
196
- ],
197
- inputs=[image_input, msg_input, max_tokens, temperature, thinking_toggle],
198
- outputs=[chatbot, msg_input],
199
- fn=chat_with_image,
200
- cache_examples=True,
201
- cache_mode="lazy",
202
- )
203
 
204
- def clear_chat():
205
- return [], ""
 
 
 
 
 
 
 
 
 
 
206
 
207
- clear_btn.click(clear_chat, outputs=[chatbot, msg_input])
208
- send_btn.click(
209
- chat_with_image,
210
- inputs=[image_input, msg_input, chatbot, max_tokens, temperature, thinking_toggle],
211
- outputs=[chatbot, msg_input],
 
 
 
 
 
 
212
  )
213
- msg_input.submit(
214
- chat_with_image,
215
- inputs=[image_input, msg_input, chatbot, max_tokens, temperature, thinking_toggle],
216
- outputs=[chatbot, msg_input],
 
 
217
  )
218
 
219
- demo.launch(mcp_server=True)
 
 
2
 
3
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
4
 
5
+ import spaces # noqa: E402 -- must be before torch
6
  import torch
7
  import gradio as gr
8
+ from transformers import AutoModelForImageTextToText, AutoProcessor
9
+
 
 
 
 
 
 
10
 
11
  MODEL_ID = "ProCreations/grug-9b"
12
+ PROCESSOR_ID = "deepreinforce-ai/Ornith-1.0-9B" # grug-9b omits preprocessor configs; base model has them
13
+
14
+ print(f"Loading processor from {PROCESSOR_ID} …")
15
+ processor = AutoProcessor.from_pretrained(PROCESSOR_ID)
16
 
17
+ print(f"Loading model from {MODEL_ID} ")
18
+ model = AutoModelForImageTextToText.from_pretrained(
 
19
  MODEL_ID,
20
+ dtype=torch.bfloat16,
21
+ attn_implementation="sdpa",
22
+ ).to("cuda")
23
+ model.eval()
24
  print("Model loaded.")
25
 
26
 
27
+ def _build_messages(history, image_path, user_text):
28
+ """Build the messages list from chat history + new user input."""
29
+ messages = []
30
+ for msg in history:
31
+ role = msg.get("role", "user")
32
+ content = msg.get("content", "")
33
+ if isinstance(content, list):
34
+ messages.append({"role": role, "content": content})
35
+ else:
36
+ messages.append({"role": role, "content": [{"type": "text", "text": str(content)}]})
37
+ # Add the new user message
38
+ user_content = []
39
+ if image_path is not None:
40
+ user_content.append({"type": "image", "image": image_path})
41
+ user_content.append({"type": "text", "text": user_text})
42
+ messages.append({"role": "user", "content": user_content})
43
+ return messages
44
+
45
+
46
+ @spaces.GPU(duration=120)
47
+ def predict(image_path, user_text, max_new_tokens, temperature, top_p, enable_thinking):
48
+ """Run a single-turn vision+text inference and return the response.
49
+
50
+ Args:
51
+ image_path: path to the uploaded image (or None for text-only).
52
+ user_text: the user's text prompt.
53
+ max_new_tokens: maximum number of tokens to generate.
54
+ temperature: sampling temperature (1.0 = greedy-ish).
55
+ top_p: nucleus sampling probability.
56
+ enable_thinking: whether to emit <think> reasoning before the answer.
57
+
58
+ Returns:
59
+ The decoded text response.
60
+ """
61
+ if not user_text.strip() and image_path is None:
62
+ return "Please provide some text or an image to analyze."
63
+
64
+ messages = _build_messages([], image_path, user_text if user_text.strip() else "Describe this image.")
65
+
66
+ text = processor.apply_chat_template(
67
+ messages,
68
+ tokenize=False,
69
+ add_generation_prompt=True,
70
+ enable_thinking=enable_thinking,
71
+ )
72
+
73
+ # Build image inputs from the messages
74
+ image_inputs = []
75
+ for msg in messages:
76
+ for item in msg.get("content", []):
77
+ if isinstance(item, dict) and item.get("type") == "image":
78
+ from PIL import Image
79
+ image_inputs.append(Image.open(item["image"]).convert("RGB"))
80
+
81
+ inputs = processor(
82
+ text=[text],
83
+ images=image_inputs if image_inputs else None,
84
+ padding=True,
85
+ return_tensors="pt",
86
+ ).to("cuda")
87
+
88
+ do_sample = temperature > 0.001
89
+ with torch.inference_mode():
90
+ output_ids = model.generate(
91
+ **inputs,
92
+ max_new_tokens=int(max_new_tokens),
93
+ do_sample=do_sample,
94
+ temperature=float(temperature) if do_sample else 1.0,
95
+ top_p=float(top_p) if do_sample else 1.0,
96
+ )
97
+
98
+ # Strip the input tokens from the output
99
+ generated = output_ids[0][inputs["input_ids"].shape[1]:]
100
+ result = processor.decode(generated, skip_special_tokens=True, clean_up_tokenization_spaces=False)
101
+ return result
102
+
103
+
104
  @spaces.GPU(duration=120)
105
+ def chat_predict(message, history, image, max_new_tokens, temperature, top_p, enable_thinking):
106
+ """Multi-turn chat with optional image. Gradio passes history as a list of
107
+ [user, assistant] tuples.
 
 
 
 
 
 
108
 
109
  Args:
110
+ message: the user's latest text message.
111
+ history: list of (user_msg, assistant_msg) tuples.
112
+ image: optional uploaded image path.
113
+ max_new_tokens: maximum tokens to generate.
114
+ temperature: sampling temperature.
115
+ top_p: nucleus sampling probability.
116
+ enable_thinking: whether to emit <think> reasoning.
117
+
118
+ Returns:
119
+ The assistant's text response.
120
  """
121
+ if not message.strip() and image is None:
122
+ return "Please provide a message or an image."
 
123
 
124
+ # Convert Gradio history format to messages
125
  messages = []
126
+ for user_msg, assistant_msg in history:
127
+ # Rebuild each prior turn as a content list
128
+ user_content = []
129
+ # We can't perfectly reconstruct which prior messages had images,
130
+ # so we store images as text references in history
131
+ user_content.append({"type": "text", "text": user_msg})
132
+ messages.append({"role": "user", "content": user_content})
133
+ if assistant_msg:
134
+ messages.append({"role": "assistant", "content": [{"type": "text", "text": assistant_msg}]})
135
+
136
+ # Add the current message
137
+ user_content = []
138
  if image is not None:
139
+ user_content.append({"type": "image", "image": image})
140
+ user_content.append({"type": "text", "text": message if message.strip() else "Describe this image."})
141
+ messages.append({"role": "user", "content": user_content})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ text = processor.apply_chat_template(
144
+ messages,
145
+ tokenize=False,
146
+ add_generation_prompt=True,
147
+ enable_thinking=enable_thinking,
 
148
  )
149
 
150
+ # Collect images from messages
151
+ image_inputs = []
152
+ for msg in messages:
153
+ for item in msg.get("content", []):
154
+ if isinstance(item, dict) and item.get("type") == "image":
155
+ from PIL import Image
156
+ image_inputs.append(Image.open(item["image"]).convert("RGB"))
157
+
158
+ inputs = processor(
159
+ text=[text],
160
+ images=image_inputs if image_inputs else None,
161
+ padding=True,
162
+ return_tensors="pt",
163
+ ).to("cuda")
164
 
165
+ do_sample = temperature > 0.001
166
+ with torch.inference_mode():
167
+ output_ids = model.generate(
168
+ **inputs,
169
+ max_new_tokens=int(max_new_tokens),
170
+ do_sample=do_sample,
171
+ temperature=float(temperature) if do_sample else 1.0,
172
+ top_p=float(top_p) if do_sample else 1.0,
173
+ )
174
 
175
+ generated = output_ids[0][inputs["input_ids"].shape[1]:]
176
+ result = processor.decode(generated, skip_special_tokens=True, clean_up_tokenization_spaces=False)
177
+ return result
178
 
179
 
180
  CSS = """
 
185
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
186
  gr.Markdown(
187
  """
188
+ # 🪨 Grug-9B Vision-Language Demo
189
+
190
+ **ProCreations/grug-9b**a 9B-parameter reasoning VLM (fine-tuned from Ornith-1.0-9B / Qwen3.5)
191
+ that "thinks small" — producing concise reasoning instead of verbose chain-of-thought.
192
+
193
+ Upload an image and ask a question, or just type a prompt. The model will respond
194
+ with a short reasoning trace followed by its answer.
195
 
196
+ [Model card](https://huggingface.co/ProCreations/grug-9b) · [Base model](https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B)
197
  """
198
  )
199
 
200
+ with gr.Row():
201
+ with gr.Column(scale=3):
202
  image_input = gr.Image(
203
+ label="Upload Image (optional)",
204
+ type="filepath",
205
+ height=320,
206
+ )
207
+ text_input = gr.Textbox(
208
+ label="Prompt",
209
+ placeholder="Ask something about the image, or type a prompt…",
210
+ lines=3,
211
+ )
212
+ submit_btn = gr.Button("Submit", variant="primary")
213
+
214
+ with gr.Column(scale=4):
215
+ output_text = gr.Textbox(
216
+ label="Response",
217
+ lines=16,
218
+ max_lines=30,
219
+ show_copy_button=True,
220
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ with gr.Accordion("Advanced settings", open=False):
223
+ with gr.Row():
224
+ max_tokens = gr.Slider(
225
+ label="Max new tokens", minimum=64, maximum=2048, value=512, step=64,
226
+ )
227
+ temperature = gr.Slider(
228
+ label="Temperature", minimum=0.0, maximum=2.0, value=0.7, step=0.05,
229
+ )
230
+ top_p = gr.Slider(
231
+ label="Top-p", minimum=0.1, maximum=1.0, value=0.9, step=0.05,
232
+ )
233
+ thinking = gr.Checkbox(label="Enable thinking (<think> tag)", value=True)
234
 
235
+ gr.Examples(
236
+ examples=[
237
+ ["examples/astronaut.jpg", "What is happening in this image?", 512, 0.7, 0.9, True],
238
+ ["examples/cat_tabby.jpg", "Describe this cat in detail.", 512, 0.7, 0.9, True],
239
+ ["examples/bird_bee_eater.jpg", "What species is this bird? What is it doing?", 512, 0.7, 0.9, True],
240
+ ],
241
+ inputs=[image_input, text_input, max_tokens, temperature, top_p, thinking],
242
+ outputs=output_text,
243
+ fn=predict,
244
+ cache_examples=True,
245
+ cache_mode="lazy",
246
  )
247
+
248
+ submit_btn.click(
249
+ fn=predict,
250
+ inputs=[image_input, text_input, max_tokens, temperature, top_p, thinking],
251
+ outputs=output_text,
252
+ api_name="predict",
253
  )
254
 
255
+ if __name__ == "__main__":
256
+ demo.launch(mcp_server=True)
examples/astronaut.jpg ADDED
examples/bird_bee_eater.jpg ADDED
requirements.txt CHANGED
@@ -1,8 +1,8 @@
1
- transformers==5.13.0
2
  accelerate
 
 
 
3
  sentencepiece
4
- qwen-vl-utils
5
  einops
6
- pillow
7
- numpy
8
- torchvision
 
1
+ transformers>=5.8
2
  accelerate
3
+ torch
4
+ torchvision
5
+ Pillow
6
  sentencepiece
 
7
  einops
8
+ numpy