Nazar
Update title and subtitle for HuggingFaceM4 demo
cfa37ec
Raw
History Blame Contribute Delete
4.43 kB
import os
from threading import Thread
import gradio as gr
import spaces
import torch
from huggingface_hub import hf_hub_download
from PIL import Image
from transformers import TextIteratorStreamer
from model import VLM, VLMConfig, transform
# ── Load the model once at startup ───────────────────────────────────────────
# The ~10GB checkpoint lives in a (private) model repo; download it with the
# Space's HF_TOKEN secret, then load the weights into our custom VLM.
CKPT_REPO = "ndrugov/encoder-free-vlm-densefusion-sharegpt4v"
CKPT_FILE = "vlm_best.pt"
ckpt_path = hf_hub_download(
repo_id=CKPT_REPO,
filename=CKPT_FILE,
token=os.environ.get("HF_TOKEN"),
)
cfg = VLMConfig()
vlm = VLM(cfg)
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
state = ckpt.get("model_state_dict", ckpt)
vlm.load_state_dict(state)
vlm.eval()
vlm.to("cuda")
print(f"Loaded {CKPT_FILE} (step={ckpt.get('step')}, "
f"best_val_loss={ckpt.get('best_val_loss')})")
N_IMG = cfg.vision.num_patches
@spaces.GPU
def model_inference(input_dict, history):
text = input_dict["text"]
files = input_dict.get("files", [])
if not files:
raise gr.Error("Please upload an image along with your question.")
if text == "":
raise gr.Error("Please type a question about the image.")
# This encoder-free VLM is trained on a single 512x512 image per turn.
image = files[-1]
if isinstance(image, str):
image = Image.open(image)
img = transform(image.convert("RGB")).unsqueeze(0).to("cuda") # (1, 3, 512, 512)
# Prompt = one <|image|> per patch, then the question, as an open chat turn.
content = vlm.tokenizer.image_token * N_IMG + text
prompt = vlm.tokenizer.apply_chat_template(
[{"role": "user", "content": content}],
tokenize=False, add_generation_prompt=True,
)
input_ids = torch.tensor([vlm.tokenizer.encode(prompt)], device="cuda")
# Splice projected patch embeddings into the <|image|> slots.
image_embd = vlm.connector(vlm.vision_embedder(img))
token_embd = vlm.decoder.get_input_embeddings()(input_ids)
combined = vlm._replace_img_tokens_with_embd(input_ids, token_embd, image_embd)
attn = torch.ones(combined.shape[:2], dtype=torch.long, device="cuda")
streamer = TextIteratorStreamer(
vlm.tokenizer, skip_prompt=True, skip_special_tokens=True
)
gen_kwargs = dict(
inputs_embeds=combined,
attention_mask=attn,
max_new_tokens=256,
do_sample=False,
pad_token_id=vlm.tokenizer.pad_token_id,
streamer=streamer,
)
def _run():
with torch.autocast(device_type="cuda", dtype=torch.bfloat16), torch.no_grad():
vlm.decoder.generate(**gen_kwargs)
thread = Thread(target=_run)
thread.start()
buffer = ""
yield "…"
for new_text in streamer:
buffer += new_text
yield buffer
examples = [
[{"text": "Describe this image.", "files": ["example_images/cat.jpeg"]}],
[{"text": "What is in this image?", "files": ["example_images/sf.jpeg"]}],
[{"text": "Describe this image in detail.", "files": ["example_images/tree.jpeg"]}],
[{"text": "Describe the scene.", "files": ["example_images/ski.jpeg"]}],
]
demo = gr.ChatInterface(
fn=model_inference,
title="Demo of Encoder-Free VLM Trained for $100",
description=(
"Play with this encoder-free vision-language model, inspired by the "
"architecture of Gemma 4 12B Unified. Our model was trained for about "
"$100 (43 hours on a single H100). It used Qwen 3 1.7B as a decoder and "
"a subset of FineVision as training data.\n\n"
"To get started, upload an image and text or try one of the examples. "
"This demo doesn't use history for the chat, so every chat you start is "
"a new conversation.\n\n"
"Read more about how this model was trained in our blogpost: "
"[Train Your Own Encoder-Free VLM in $100]"
"(https://huggingface.co/spaces/ndrugov/encoder-free-vlm)."
),
examples=examples,
textbox=gr.MultimodalTextbox(
label="Query Input", file_types=["image"], file_count="single"
),
stop_btn="Stop Generation",
multimodal=True,
cache_examples=False,
)
if __name__ == "__main__":
demo.launch()