Spaces:
Running on Zero
Running on Zero
| 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." | |
| 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() | |