Views
No views yet
1import os
2import gc
3import torch
4import io
5import base64
6import gradio as gr
7from PIL import Image
8from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
9from qwen_vl_utils import process_vision_info
10
11MODEL_DIR_DEFAULT = "../MP3L-Qwen3-VL-8B-Instruct/"
12
13_loaded = {"model": None, "processor": None}
14
15def unload_model():
16 if _loaded["model"] is None:
17 return "No model currently loaded."
18 try:
19 del _loaded["model"]
20 del _loaded["processor"]
21 except Exception:
22 pass
23 _loaded["model"] = None
24 _loaded["processor"] = None
25 gc.collect()
26 if torch.cuda.is_available():
27 torch.cuda.empty_cache()
28 torch.cuda.ipc_collect()
29 return "✅ Model unloaded and memory cleaned up."
30
31
32def _parse_gpu_ids(gpu_ids_str: str):
33 """
34 Input example: "0" / "0,1" / " 1 , 3 "
35 Returns: [0,1] / None (meaning no restriction)
36 """
37 s = (gpu_ids_str or "").strip()
38 if not s:
39 return None
40 ids = []
41 for p in s.replace(" ", "").split(","):
42 if p == "":
43 continue
44 ids.append(int(p))
45 ids = sorted(set(ids))
46 return ids
47
48
49def load_model(model_dir, device_pref, dtype_pref, use_flash_attn2, gpu_ids_str, gpu_max_memory, cpu_max_memory):
50 """
51 - device_pref = auto/cuda/cpu
52 - gpu_ids_str: e.g., "0,1" restricts to those GPUs (via max_memory)
53 - gpu_max_memory: e.g., "20GiB"
54 - cpu_max_memory: e.g., "64GiB"
55 """
56 unload_model()
57
58 attn_impl = "flash_attention_2" if use_flash_attn2 else None
59 dtype = "auto" if dtype_pref == "auto" else getattr(torch, dtype_pref)
60
61 gpu_ids = _parse_gpu_ids(gpu_ids_str)
62
63 # Key: limit GPU usage via max_memory
64 # transformers will automatically split the model based on devices in max_memory
65 max_memory = None
66 device_map = None
67
68 if device_pref == "cpu" or not torch.cuda.is_available():
69 device_map = None
70 max_memory = None
71 else:
72 # CUDA available
73 if device_pref == "auto":
74 device_map = "auto"
75 else:
76 # device_pref == "cuda": still use "auto" to allow cpu offload if needed
77 device_map = "auto"
78
79 if gpu_ids is not None:
80 max_memory = {i: gpu_max_memory for i in gpu_ids}
81 # Provide a CPU fallback (to avoid OOM if GPUs are full)
82 if cpu_max_memory and cpu_max_memory.strip():
83 max_memory["cpu"] = cpu_max_memory.strip()
84
85 model = Qwen3VLForConditionalGeneration.from_pretrained(
86 model_dir,
87 dtype=dtype,
88 device_map=device_map,
89 max_memory=max_memory,
90 attn_implementation=attn_impl,
91 )
92 model.eval()
93 processor = AutoProcessor.from_pretrained(model_dir)
94
95 _loaded["model"] = model
96 _loaded["processor"] = processor
97
98 msg = f"✅ Model loaded: {model_dir}\n- device_map={device_map}"
99 if max_memory is not None:
100 msg += f"\n- max_memory={max_memory}"
101 if gpu_ids is not None:
102 msg += f"\n- Restricted GPU IDs={gpu_ids}"
103 return msg
104
105
106def load_image_from_path(path: str):
107 p = (path or "").strip()
108 if not p:
109 return None, "❌ Please enter an image path."
110 if not os.path.isfile(p):
111 return None, f"❌ File does not exist: {p}"
112 try:
113 img = Image.open(p).convert("RGB")
114 return img, f"✅ Image loaded: {p}"
115 except Exception as e:
116 return None, f"❌ Failed to load image: {repr(e)}"
117
118def _md_linebreak(s: str) -> str:
119 """Make line breaks render properly in Markdown."""
120 s = (s or "").replace("\r\n", "\n")
121 return s.replace("\n", " \n") # two spaces + newline = Markdown forced line break
122
123def _pil_to_data_uri(img: Image.Image, max_side: int = 512) -> str:
124 """PIL -> data:image/png;base64,... (resize to avoid huge strings)."""
125 if img is None:
126 return ""
127 im = img.convert("RGB")
128 w, h = im.size
129 scale = max(w, h) / float(max_side)
130 if scale > 1:
131 im = im.resize((int(w / scale), int(h / scale)))
132 buf = io.BytesIO()
133 im.save(buf, format="PNG")
134 b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
135 return f"data:image/png;base64,{b64}"
136
137def chat_generate(ui_msgs, qwen_msgs, user_text, user_image, max_new_tokens, temperature, top_p):
138 """
139 ui_msgs : messages for gr.Chatbot display (content as markdown string, possibly containing base64 images)
140 qwen_msgs : messages for Qwen3-VL inference (content is a list of dicts with type:text/image, images as PIL)
141 """
142 if _loaded["model"] is None:
143 return ui_msgs, qwen_msgs, None, "❌ Model not loaded. Please load the model first."
144 if (not user_text or not user_text.strip()) and user_image is None:
145 return ui_msgs, qwen_msgs, None, "❌ Please provide at least text or an image."
146
147 model = _loaded["model"]
148 processor = _loaded["processor"]
149
150 # -------- UI display: embed image in markdown with visible line breaks --------
151 ui_user_parts = []
152 if user_text and user_text.strip():
153 ui_user_parts.append(_md_linebreak(user_text.strip()))
154 if user_image is not None:
155 data_uri = _pil_to_data_uri(user_image, max_side=512)
156 ui_user_parts.append(f"")
157 ui_user_display = "\n\n".join(ui_user_parts) if ui_user_parts else "[Image]"
158
159 ui_msgs = list(ui_msgs or [])
160 ui_msgs.append({"role": "user", "content": ui_user_display})
161
162 # -------- Model inference: preserve raw structure (don't put base64 into model context) --------
163 qwen_msgs = list(qwen_msgs or [])
164 user_content = []
165 if user_image is not None:
166 user_content.append({"type": "image", "image": user_image})
167 if user_text and user_text.strip():
168 user_content.append({"type": "text", "text": user_text.strip()})
169 qwen_msgs.append({"role": "user", "content": user_content})
170
171 # Assemble inputs
172 inputs = processor.apply_chat_template(
173 qwen_msgs,
174 tokenize=True,
175 add_generation_prompt=True,
176 return_dict=True,
177 return_tensors="pt",
178 )
179
180 # Move to the device where the model resides (supports user-specified GPU)
181 try:
182 target_device = next(model.parameters()).device
183 except StopIteration:
184 target_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
185 inputs = inputs.to(target_device)
186
187 gen_kwargs = {
188 "max_new_tokens": int(max_new_tokens),
189 "do_sample": float(temperature) > 0,
190 "temperature": float(temperature),
191 "top_p": float(top_p),
192 }
193
194 with torch.inference_mode():
195 out = model.generate(**inputs, **gen_kwargs)
196
197 out_trim = [o[len(i):] for i, o in zip(inputs.input_ids, out)]
198 resp = processor.batch_decode(out_trim, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
199
200 # Update both histories
201 ui_msgs.append({"role": "assistant", "content": _md_linebreak(resp)})
202 qwen_msgs.append({"role": "assistant", "content": [{"type": "text", "text": resp}]})
203
204 # Return: ui_msgs for Chatbot; clear input image (img output None), status message
205 return ui_msgs, qwen_msgs, None, "✅ Response generated (image cleared but remains in chat)."
206
207def clear_chat():
208 return [], [], None, "✅ Chat cleared."
209
210
211with gr.Blocks(title="Qwen3-VL merged local inference WebUI") as demo:
212 gr.Markdown("## Qwen3-VL (merged checkpoint) local deployment and inference")
213
214 model_dir = gr.Textbox(label="Model directory", value=MODEL_DIR_DEFAULT)
215
216 with gr.Row():
217 device_pref = gr.Dropdown(choices=["auto", "cuda", "cpu"], value="auto", label="Device")
218 dtype_pref = gr.Dropdown(choices=["auto", "float16", "bfloat16", "float32"], value="auto", label="dtype")
219 use_flash_attn2 = gr.Checkbox(value=False, label="flash_attention_2 (enable if environment supports)")
220
221 with gr.Row():
222 gpu_ids_str = gr.Textbox(
223 label="Restrict to GPU IDs (optional)",
224 placeholder='e.g., "0" or "0,1"; leave empty for no restriction',
225 value=""
226 )
227 gpu_max_memory = gr.Textbox(
228 label="Max memory per GPU (max_memory)",
229 placeholder='e.g., "20GiB"',
230 value="20GiB"
231 )
232 cpu_max_memory = gr.Textbox(
233 label="CPU fallback max_memory (optional)",
234 placeholder='e.g., "64GiB"; leave empty to skip',
235 value="64GiB"
236 )
237
238 with gr.Row():
239 btn_load = gr.Button("Load model", variant="primary")
240 btn_unload = gr.Button("Unload model", variant="stop")
241 btn_clear = gr.Button("Clear chat")
242
243 status = gr.Markdown("(Status display)")
244
245 chatbot = gr.Chatbot(label="Chat", height=520, type="messages")
246 state_ui = gr.State([]) # for Chatbot display
247 state_qwen = gr.State([]) # for model inference context (with images)
248
249 with gr.Row():
250 with gr.Column():
251 img_path = gr.Textbox(label="Load image from path (optional)", placeholder="/abs/path/to/image.jpg")
252 btn_load_img = gr.Button("Load image to panel")
253 img = gr.Image(type="pil", label="Uploaded image (optional)", height=300)
254
255 with gr.Column():
256 user_text = gr.Textbox(lines=6, label="Input text (optional)")
257 with gr.Row():
258 max_new_tokens = gr.Slider(1, 2048, value=256, step=1, label="max_new_tokens")
259 temperature = gr.Slider(0.0, 1.5, value=0.2, step=0.05, label="temperature")
260 top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="top_p")
261 btn_send = gr.Button("Send", variant="primary")
262
263 # Load image button: read path into img component
264 btn_load_img.click(load_image_from_path, [img_path], [img, status])
265
266 # Load/unload model
267 btn_load.click(
268 load_model,
269 [model_dir, device_pref, dtype_pref, use_flash_attn2, gpu_ids_str, gpu_max_memory, cpu_max_memory],
270 status
271 )
272 btn_unload.click(unload_model, [], status)
273
274 # Clear chat
275 btn_clear.click(clear_chat, [], [chatbot, state_qwen, img, status]).then(lambda: [], [], state_ui)
276
277 btn_send.click(
278 chat_generate,
279 [state_ui, state_qwen, user_text, img, max_new_tokens, temperature, top_p],
280 [chatbot, state_qwen, img, status],
281 ).then(lambda x: x, chatbot, state_ui)
282
283demo.queue(max_size=32).launch(
284 server_name="0.0.0.0",
285 server_port=None, # automatically find a free port
286 show_api=False
287)1**Englsih Version:**
2Input: An image-text pair: <image> <text>.
3Task: Perform a comprehensive metaphor analysis.
4Process: Internally evaluate the input for metaphorical content. Do not output your reasoning steps, only the final assessment.
5Output Requirements:
61. Metaphor Presence: State "Yes" or "No".
72. If "Yes":
8 - Emotional Perspective: [Explanation]
9 - Intentional Perspective: [Explanation]
10 - Offensive Perspective: [Explanation]
11 - Mapping Process: Describe the 'source' and 'target' domains of the metaphor.
123. If "No":
13 - Reason: Provide a concise explanation for the absence of metaphor.
14
15**中文版本:**
16输入:一个图文对:<image> <text>。
17任务:执行全面的隐喻分析。
18过程:请在内部对输入内容进行隐喻评估。无需输出推理步骤,仅提供最终评估结果。
19输出要求:
201. 隐喻存在性:回答“是”或“否”。
212. 如果为“是”:
22 - 情感视角:[解释]
23 - 意图视角:[解释]
24 - 冒犯视角:[解释]
25 - 映射过程:描述隐喻的“源域”和“目标域”。
263. 如果为“否”:
27 - 原因:简要说明不存在隐喻的原因。