File size: 7,968 Bytes
d0e7c02
 
 
 
 
 
 
 
 
 
 
 
 
0c5d1aa
d0e7c02
 
 
 
0c5d1aa
d0e7c02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18bf402
 
 
d0e7c02
0c5d1aa
d0e7c02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Reading slots in-process with transformers: no server. One forward pass computes every slot of a layout
(only the logits at the slot positions, logits_to_keep), exactly as training reads them. Used for the merged
models and the Hugging Face Space (ZeroGPU runs PyTorch, not vLLM).

  from slotread.local import LocalSlotModel
  m = LocalSlotModel.from_pretrained("infinitylogesh/GUI-Decisions-31B")          # merged model
  m = LocalSlotModel.from_pretrained("google/gemma-4-31B-it", adapter="infinitylogesh/GUI-Decisions-31B-LoRA")
  pred = m.decide(Image.open("shot.png"), "open the settings")    # {'action': 'click', 'x': ..., 'y': ...}

The slot template and label ids come from slotread_config.json in the model (or adapter) repo: the
config.json that slotread.train writes, renamed so it does not collide with the model's own config.json.
"""

import json, time

import torch

from . import labels as L
from .client import agent_user, load_config
from .images import cap_image
from .labels import Arm
from .tasks import AgentTask, BoxTask, SegTask

class LocalSlotModel:
    def __init__(self, model, processor, cfg):
        self.model, self.proc, self.cfg = model, processor, cfg
        self.arm = Arm(cfg["arm"], cfg["coarse"], cfg["bins"], cfg["tokens"], cfg["fine"])
        self.tasks = {"agent": AgentTask(), "box": BoxTask(), "seg": SegTask(cfg.get("rays", 24))}
        L.PH = cfg["placeholder"]  # the placeholder id of this model's tokenizer

    @classmethod
    def from_pretrained(cls, model_id, adapter=None, processor=None, config=None, dtype=torch.bfloat16, device_map=None):
        """config: a slotread config file; default: slotread_config.json in the adapter or model repo."""
        from transformers import AutoModelForImageTextToText, AutoProcessor
        model = AutoModelForImageTextToText.from_pretrained(model_id, dtype=dtype, device_map=device_map or {"": 0},
                                                            attn_implementation="sdpa")
        if adapter:
            from peft import PeftModel
            # on the model's device: under ZeroGPU CUDA looks available before a GPU is attached
            kw = {"torch_device": "cpu"} if device_map == "cpu" else {}
            model = PeftModel.from_pretrained(model, adapter, **kw)
        model.eval()
        cfg = json.load(open(config)) if config else load_config(adapter or model_id)
        return cls(model, AutoProcessor.from_pretrained(processor or model_id), cfg)

    @torch.no_grad()
    def read(self, task, image, user):
        """{slot: log-probabilities over its labels} for the task's whole layout, one forward pass;
        also the forward's milliseconds."""
        t = self.tasks[task]
        c = self.cfg["tasks"][task]
        image = cap_image(image.convert("RGB"), self.cfg.get("image_cap") or 0)  # as trained (GUI-Owl: 1 MP)
        ex = {"agent": dict(image=image, user=user), "box": dict(image=image, instruction=user),
              "seg": dict(image=image, sentence=user)}[task]
        msgs = [{"role": "system", "content": [{"type": "text", "text": c["system"]}]},
                {"role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": t.user(ex)}]},
                {"role": "assistant", "content": [{"type": "text", "text": " ".join(f"{s}: ___" for s in c["slots"])}]}]
        enc = self.proc.apply_chat_template(msgs, tokenize=True, return_dict=True, return_tensors="pt",
                                           add_generation_prompt=False, continue_final_message=True, enable_thinking=False)
        ids = enc["input_ids"][0]
        keep = (ids == self.cfg["placeholder"]).nonzero().flatten()[-len(c["slots"]):] - 1
        dev = next(self.model.parameters()).device
        if dev.type == "cuda":
            torch.cuda.synchronize()
        t0 = time.perf_counter()
        out = self.model(**{k: v.to(dev) for k, v in enc.items()}, logits_to_keep=keep.to(dev), use_cache=False)
        lv = torch.log_softmax(out.logits[0].float(), -1)
        if dev.type == "cuda":
            torch.cuda.synchronize()
        ms = (time.perf_counter() - t0) * 1000
        return {s: lv[i, c["labels"][s]].cpu() for i, s in enumerate(c["slots"])}, ms

    def decide(self, image, instruction, previous="None"):
        """A computer-use step from one forward pass: action and point, plus the swipe end, key and scroll
        slots (meaningful only for their actions); `ms` is the forward pass."""
        from .data.aguvis import KEYS, SDIR
        lp, ms = self.read("agent", image, agent_user(instruction, previous))
        d = self.tasks["agent"].decode(lp, self.arm)
        d.update(key=KEYS[int(lp["key"].argmax())], sdir=SDIR[int(lp["sdir"].argmax())], samt=int(lp["samt"].argmax()))
        return dict(d, ms=round(ms, 1))

    def box(self, image, description):
        lp, ms = self.read("box", image, description)
        return dict(box=self.tasks["box"].decode(lp, self.arm), ms=round(ms, 1))

    @torch.no_grad()
    def write_text(self, image, instruction, action, previous="None", max_new_tokens=24):
        """The text a type / answer / open step needs, written by the base model: the adapter is switched
        off, so this needs base + adapter (a merged model cannot switch it off)."""
        from .agent import TEXT_ASK
        if not hasattr(self.model, "disable_adapter"):
            raise RuntimeError("text steps need the base model: load with adapter=...")
        image = cap_image(image.convert("RGB"), self.cfg.get("image_cap") or 0)
        msgs = [{"role": "user", "content": [{"type": "image", "image": image},
                                             {"type": "text", "text": agent_user(instruction, previous) + "\n\n" + TEXT_ASK[action]}]}]
        enc = self.proc.apply_chat_template(msgs, tokenize=True, return_dict=True, return_tensors="pt",
                                           add_generation_prompt=True, enable_thinking=False)
        dev = next(self.model.parameters()).device
        enc = {k: v.to(dev) for k, v in enc.items()}
        with self.model.disable_adapter():
            out = self.model.generate(**enc, max_new_tokens=max_new_tokens, do_sample=False)
        t = self.proc.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
        return t.strip().strip('"').split("\n")[0][:120]

    @torch.no_grad()
    def vanilla_decide(self, image, instruction, previous="None", max_new_tokens=96):
        """The base model (adapter off) writing the step as JSON in Gemma 4's native [y, x] format, as in
        the post's comparisons: (action, point, args, text, raw output, ms for the whole generation)."""
        from .agent import json_system, parse_step
        if not hasattr(self.model, "disable_adapter"):
            raise RuntimeError("the vanilla comparison needs the base model: load with adapter=...")
        image = cap_image(image.convert("RGB"), self.cfg.get("image_cap") or 0)
        msgs = [{"role": "system", "content": [{"type": "text", "text": json_system(True)}]},
                {"role": "user", "content": [{"type": "image", "image": image},
                                             {"type": "text", "text": agent_user(instruction, previous)}]}]
        enc = self.proc.apply_chat_template(msgs, tokenize=True, return_dict=True, return_tensors="pt",
                                           add_generation_prompt=True, enable_thinking=False)
        dev = next(self.model.parameters()).device
        enc = {k: v.to(dev) for k, v in enc.items()}
        torch.cuda.synchronize()
        t0 = time.perf_counter()
        with self.model.disable_adapter():
            out = self.model.generate(**enc, max_new_tokens=max_new_tokens, do_sample=False)
        torch.cuda.synchronize()
        ms = (time.perf_counter() - t0) * 1000
        txt = self.proc.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
        return parse_step(txt, True) + (txt, round(ms, 1))