File size: 8,457 Bytes
305bf96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4e530
 
305bf96
 
 
 
5d4e530
 
 
 
305bf96
 
 
5d4e530
 
305bf96
 
15b8db1
 
 
 
 
 
 
305bf96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4e530
 
 
 
 
 
305bf96
 
 
 
 
 
 
5d4e530
 
 
 
 
6bf0a8f
 
5d4e530
6bf0a8f
5d4e530
 
305bf96
 
5d4e530
305bf96
 
5d4e530
 
305bf96
 
 
 
 
 
 
5d4e530
 
 
305bf96
5d4e530
 
 
 
 
 
 
305bf96
 
 
 
 
5d4e530
15b8db1
 
 
 
305bf96
15b8db1
 
 
305bf96
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import os
import threading
import time

import gradio as gr
import numpy as np
import spaces
import torch
from huggingface_hub import hf_hub_download, snapshot_download
from omegaconf import OmegaConf
from PIL import Image
from transformers import StoppingCriteriaList

from minigpt4.conversation.conversation import (
    CONV_VISION_Vicuna0, Chat, StoppingCriteriaSub)
from minigpt4.models.minigpt4 import MiniGPT4
from minigpt4.processors.blip_processors import Blip2ImageEvalProcessor

# Vicuna v0 normally needs LLaMA-1 base weights plus a delta, and LLaMA-1 was never
# publicly distributed. These are the authors' own prepared weights, which is the
# only reason this model can still be assembled at all.
VICUNA_REPO = "Vision-CAIR/vicuna-7b"
# The trained linear projection. Everything else (ViT-G, Q-Former) stays frozen.
CKPT_REPO, CKPT_FILE = "ckpt/minigpt4-7B", "prerained_minigpt4_7b.pth"

MODEL = None
CHAT = None
VISION_OK = None
LOCK = threading.Lock()
_ON_GPU = False


def build():
    global MODEL, CHAT, VISION_OK
    vicuna = snapshot_download(VICUNA_REPO)
    ckpt = hf_hub_download(CKPT_REPO, CKPT_FILE)

    cfg = OmegaConf.load("minigpt4/configs/models/minigpt4_vicuna0.yaml").model
    cfg.llama_model = vicuna
    cfg.ckpt = ckpt
    cfg.low_resource = False          # 24 GB of A10G, no need for the 8-bit path
    cfg.prompt_template = "###Human: {} ###Assistant: "
    cfg.max_txt_len = 160
    cfg.end_sym = "###"

    MODEL = MiniGPT4.from_config(cfg).eval()
    vis = Blip2ImageEvalProcessor.from_config(
        OmegaConf.create({"name": "blip2_image_eval", "image_size": 224}))

    # "###" and "##" as stop sequences, straight from the reference demo.
    stops = [torch.tensor(ids) for ids in ([835], [2277, 29937])]
    CHAT = Chat(MODEL, vis, device="cpu",
                stopping_criteria=StoppingCriteriaList([StoppingCriteriaSub(stops=stops)]))
    print("[mg4] model built on CPU", flush=True)


def to_gpu():
    """ZeroGPU only exposes CUDA inside a @spaces.GPU call, so the move happens on
    first use rather than at import."""
    global _ON_GPU
    if _ON_GPU:
        return
    dev = torch.device("cuda")
    MODEL.to(dev)
    CHAT.device = dev
    CHAT.stopping_criteria = StoppingCriteriaList(
        [StoppingCriteriaSub(stops=[torch.tensor(i).to(dev) for i in ([835], [2277, 29937])])])
    _ON_GPU = True
    print("[mg4] moved to CUDA", flush=True)


def _embed(img):
    lst = []
    conv = CONV_VISION_Vicuna0.copy()
    CHAT.upload_img(img, conv, lst)
    CHAT.encode_img(lst)
    return lst[0].detach().float().cpu().numpy()


@spaces.GPU(duration=120)
def selftest():
    """The CPU ggml port returned bit-identical embeddings for any two images, which
    is what made every answer confabulated. Prove this build does not, and time the
    encode while we are here so the header can quote a real number."""
    to_gpu()
    black = Image.new("RGB", (448, 448), (0, 0, 0))
    patt = Image.fromarray(
        (np.indices((448, 448)).sum(0) % 256).astype("uint8")).convert("RGB")
    a = _embed(black)
    t = time.time()
    b = _embed(patt)
    enc = time.time() - t
    diff = float(np.abs(a - b).max())
    ok = diff > 1e-4
    print(f"[mg4] SELFTEST vision responds to input: {ok} (maxdiff={diff:.6f} "
          f"shape={a.shape} encode={enc:.2f}s)", flush=True)
    return ok, enc


def example_run(image, prompt):
    """The example row carries only the two inputs, so the sliders keep their
    defaults. Routed through chat() so a cached example is the same code path
    the Send button runs."""
    return chat(image, prompt, [], 300, 0.7, 0.9)


@spaces.GPU(duration=120)
def chat(image, prompt, history, max_new, temperature, top_p):
    history = history or []
    if image is None:
        raise gr.Error("Upload an image first. This model always answers about an image.")
    if not (prompt or "").strip():
        raise gr.Error("Type a question about the image.")
    if not LOCK.acquire(blocking=False):
        raise gr.Error("Another request is running. Try again in a moment.")
    try:
        to_gpu()
        t0 = time.time()
        conv = CONV_VISION_Vicuna0.copy()
        img_list = []
        CHAT.upload_img(image, conv, img_list)
        CHAT.encode_img(img_list)
        enc = time.time() - t0

        CHAT.ask(prompt, conv)
        t1 = time.time()
        answer = CHAT.answer(conv=conv, img_list=img_list,
                             num_beams=1, temperature=float(temperature),
                             top_p=float(top_p), max_new_tokens=int(max_new),
                             max_length=2000)[0]
        dt = time.time() - t1
        answer = answer.replace("<Img>", "").replace("</Img>", "").strip()
        note = f"\n\n`encode {enc:.1f}s · generate {dt:.1f}s`"
        return history + [{"role": "user", "content": prompt},
                          {"role": "assistant", "content": answer + note}], ""
    finally:
        LOCK.release()


CSS = """
footer{display:none}
.gap{gap:0}
.block{padding:2px}
#img .upload-container,#img .file-preview{max-height:70px}
#img img{max-height:150px;object-fit:contain}
#hdr{max-height:32px}
#hdr p{margin:0}
:where(button,input,textarea,select,[role="button"],[tabindex]):focus-visible{
  outline:2px solid #7c5cff!important;outline-offset:2px}
@media (prefers-reduced-motion:reduce){*,*::before,*::after{
  animation-duration:.01ms!important;transition-duration:.01ms!important}}
"""

build()
VISION_OK, ENC_S = selftest()

# One line, resolved backend and measured cost, so the wall clock is predictable
# before anyone uploads anything. Generation is ~9 ms/token on this hardware.
GPU_NAME = "ZeroGPU A10G"
# Sub-second encodes read as "0.0s" at one decimal, which looks like broken telemetry.
ENC_TXT = f"{ENC_S * 1000:.0f}ms" if ENC_S < 0.5 else f"{ENC_S:.1f}s"
HEADER = (f"**MiniGPT-4 7B** · uncensored captioning · Vicuna v0 · {GPU_NAME} · "
          f"~{ENC_TXT} encode + ~{300 * 0.009:.0f}s per 300 tokens"
          + ("" if VISION_OK else "  ·  ⚠ **vision self-test FAILED, answers are "
                                  "not grounded in the image**"))

with gr.Blocks(fill_width=True) as demo:
    gr.Markdown(HEADER, elem_id="hdr")
    with gr.Row():
        with gr.Column(scale=3):
            img = gr.Image(type="pil", show_label=False, elem_id="img", height=150)
            box = gr.Chatbot(show_label=False, height=230)
            with gr.Row():
                msg = gr.Textbox(placeholder="Ask about the image...", show_label=False,
                                 container=False, lines=2, max_lines=2, scale=5)
                go = gr.Button("Send", variant="primary", scale=1)
            wipe = gr.Button("New conversation", size="sm")
        with gr.Column(scale=1):
            gr.Markdown(
                "Vicuna v0 predates the refusal training that arrived later in 2023, so "
                "it describes what it is shown instead of declining. It also "
                "hallucinates freely. A 2023 model, not a current one."
            )
            with gr.Accordion("Settings", open=False):
                max_new = gr.Slider(32, 512, value=300, step=32,
                                    label="Max new tokens (300 ≈ 3s)")
                temperature = gr.Slider(0.1, 1.5, value=0.7, step=0.05,
                                        label="Temperature",
                                        info="Higher wanders further from the image.")
                top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top P")

    go.click(chat, [img, msg, box, max_new, temperature, top_p], [box, msg])
    msg.submit(chat, [img, msg, box, max_new, temperature, top_p], [box, msg])
    wipe.click(lambda: ([], ""), outputs=[box, msg], queue=False)

    # Exactly one row. Every row is a full pipeline run if caching is ever enabled.
    # With no fn the example only fills the inputs and the visitor still has to
    # press Send, which reads as a broken example. Give it the handler and cache
    # it. Lazy rather than eager because caching at startup would need the GPU
    # before ZeroGPU has handed one over.
    gr.Examples(examples=[["examples/cats.jpg", "Describe this image in detail."]],
                inputs=[img, msg], outputs=[box, msg],
                fn=example_run, cache_examples=True, cache_mode="lazy",
                label="Example")

demo.queue(default_concurrency_limit=1, max_size=8).launch(css=CSS, show_error=True)