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()