File size: 8,784 Bytes
708b82a
 
 
 
 
26b4221
708b82a
 
 
 
26b4221
 
 
 
 
 
708b82a
 
 
 
26b4221
708b82a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b014161
 
 
 
 
 
 
 
 
708b82a
b014161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708b82a
 
b014161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708b82a
 
 
 
 
b014161
 
708b82a
 
 
 
 
26b4221
 
 
 
 
 
 
 
 
 
 
 
 
 
708b82a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26b4221
708b82a
 
 
 
26b4221
708b82a
26b4221
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
---
license: apache-2.0
base_model: google/gemma-4-31B-it
library_name: transformers
pipeline_tag: image-text-to-text
tags: [computer-use, gui-agent, gui-grounding, visual-grounding, slot-reading]
---

# GUI-Decisions-31B

[![Slot read vs writing JSON: the same model on the same Wikipedia task, 4 decisions in 0.63 s against 2.08 s](assets/hero.gif)](https://huggingface.co/spaces/infinitylogesh/GUI-Decisions)

*Same model, same screenshots: reading slots (left, 158 ms per decision) against writing the step as JSON
(right, 520 ms). [Try it in the Space](https://huggingface.co/spaces/infinitylogesh/GUI-Decisions).*

[google/gemma-4-31B-it](https://huggingface.co/google/gemma-4-31B-it) with the slot-reading LoRA merged in (bf16), which makes
computer-use decisions **readable from one forward pass** instead of decoded: the action, the point
(256 bins per axis), a swipe's end, the key and the scroll, all as next-token distributions at fixed
slots. It also reads boxes (centre + log size) and coarse masks (centre + 24 rays) for general objects.

From the blog post [Stop Decoding Coordinates: 3× Faster Computer-Use Grounding](https://logeshumapathi.com/blog/2026/10/06/slot-reading.html)
(run E3). The LoRA alone: [GUI-Decisions-31B-LoRA](https://huggingface.co/infinitylogesh/GUI-Decisions-31B-LoRA).
Try it: [GUI-Decisions Space](https://huggingface.co/spaces/infinitylogesh/GUI-Decisions).

## How it works

The assistant turn is a fixed template; every slot ends in a placeholder (` ___`):

```
action: ___ x: ___ y: ___ x2: ___ y2: ___ key: ___ sdir: ___ samt: ___
```

A slot's answer is the next-token distribution at the token before its placeholder, restricted to the
slot's labels (`slotread_config.json` lists them: action words, and 256 single-token "byte" labels per
coordinate). Training used cross-entropy against soft ordinal (Gaussian) targets, so the bins form a
real distribution over the screen.

## Use

### With vLLM

Served in bf16 on one RTX PRO 6000 a computer-use step takes **190 ms** (median over 60 cold screenshots).
[The LoRA on the NVFP4 base](https://huggingface.co/infinitylogesh/GUI-Decisions-31B-LoRA) is faster
(152 ms) and keeps the base model for text steps; use this merged model when you want one plain
checkpoint.

**1. Install** vLLM 0.30 and the reader (`slotread`, from the code repository):

```bash
git clone https://github.com/infinitylogesh/gui-decisions && cd gui-decisions
pip install "vllm==0.30.*" -e .
bash serving/patch_vllm.sh                 # recommended: 256 label ids per request
```

**2. Serve** (bf16, 62 GB of weights: one 80–96 GB GPU):

```bash
vllm serve infinitylogesh/GUI-Decisions-31B --served-model-name gui-decisions --port 8001 \
  --max-model-len 8192 --gpu-memory-utilization 0.85 --max-logprobs 300 --enable-prefix-caching \
  --mm-processor-cache-gb 0 --api-server-count 8
```

**3. Read** steps and boxes:

```python
from PIL import Image
from slotread.client import SlotModel

m = SlotModel("gui-decisions", "infinitylogesh/GUI-Decisions-31B")   # server: VLLM_URL, default http://127.0.0.1:8001
step = m.decide(Image.open("screenshot.png"), "turn on dark mode")   # {'action': 'click', 'x': 0.928, 'y': 0.247, 'ms': 190.0}
box = m.box(Image.open("photo.jpg"), "the red frisbee")              # {'box': [x1, y1, x2, y2], 'ms': ...}, in [0, 1]
```

`decide` reads `action`, `x` and `y` first, then only the slots the action needs (`key` for press, `sdir`
and `samt` for scroll, `x2` and `y2` for swipe). `x` and `y` are fractions of the screenshot's width and
height. For type / answer steps, serve the base model as well and pass its name as `text_model=` (the
merged weights only answer slots).

**What a read is.** For each slot, `SlotModel` sends one chat request whose assistant turn is prefilled up
to that slot, and asks vLLM for the log-probabilities of exactly that slot's labels; nothing is generated
beyond one token. The requests for one step run in parallel and share the image through vLLM's prefix cache:

```json
{"model": "gui-decisions",
 "messages": [{"role": "system", "content": [{"type": "text", "text": "<the task's system prompt, from slotread_config.json>"}]},
              {"role": "user", "content": [{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}},
                                           {"type": "text", "text": "Please generate the next move according to the UI screenshot, instruction and previous actions.\n\nInstruction: turn on dark mode\n\nPrevious actions:\nNone"}]},
              {"role": "assistant", "content": [{"type": "text", "text": "action: ___ x:"}]}],
 "add_generation_prompt": false, "continue_final_message": true, "chat_template_kwargs": {"enable_thinking": false},
 "max_tokens": 1, "temperature": 0, "logprobs": true, "top_logprobs": 1, "return_tokens_as_token_ids": true,
 "logprob_token_ids": ["<the 256 label ids of slot x, from slotread_config.json>"]}
```

The slot's answer is the softmax over those labels: the argmax for `action`; for a coordinate, the argmax
bin refined by its ±2 neighbours (bin *k* of 256 is the value (*k* + 0.5) / 256 across the screen).

Notes:

- `patch_vllm.sh` raises vLLM's per-request `logprob_token_ids` cap from 128 to 256, so a 256-bin slot is
  one request. Without it, set `MAX_LABEL_IDS=128` for the client (each coordinate then takes two requests).
- `--api-server-count 8`: a single API-server process serializes a step's parallel slot requests.
  `--mm-processor-cache-gb 0`: vLLM 0.30's media cache raced on concurrent requests for the same image.
- Screenshots go as JPEG at the model's input size (896 × 896 pixels' worth), the fast path. For tiny
  targets send lossless full-size images instead (`IMG_FMT=png IMG_MAX_PX=0`), as the evaluations did.
- `previous` takes the earlier steps as numbered lines, e.g. `"1. click(One way)\n2. click(From)"`.

### In-process with transformers (no server)

```python
from PIL import Image
from slotread.local import LocalSlotModel

m = LocalSlotModel.from_pretrained("infinitylogesh/GUI-Decisions-31B")
step = m.decide(Image.open("screenshot.png"), "turn on dark mode")
box = m.box(Image.open("photo.jpg"), "the red frisbee")
```

## Results

This merged model in bf16 with transformers (`slotread.local`), against the published evaluation of the
LoRA (NVFP4 base, vLLM), case by case on the same sets (paired bootstrap 95% interval on the difference):

| evaluation | this model | published LoRA | difference |
| --- | --- | --- | --- |
| ScreenSpot clicks (1,272): point inside the element | 0.869 | 0.866 | +0.003 [−0.006, +0.013] |
| computer-use steps, held-out AGUVIS (1,600) | 0.650 | 0.659 | −0.009 [−0.021, +0.003] |
| RefCOCO val photo boxes (3,811): mean IoU | 0.770 | 0.770 | +0.000 [−0.003, +0.003] |
| COCO val2017 boxes (1,000): mean IoU | 0.744 | 0.742 | +0.002 [−0.002, +0.006] |

No difference is significant. Merging rounds the LoRA's small weight changes into bf16: against the
unmerged LoRA on the same bf16 base (0.659 on computer-use steps, exactly the published score) the
merged model trends lower on steps (−0.009 [−0.019, +0.001], McNemar p 0.08; 38 of 1,600 actions
differ), while clicks and boxes are unchanged. For the closest match to the published numbers, or to
write text steps, use [the LoRA](https://huggingface.co/infinitylogesh/GUI-Decisions-31B-LoRA) on the base model. A forward pass reading every slot takes 150–210 ms in plain transformers
(bf16, one RTX PRO 6000); served on vLLM with NVFP4 weights a step takes ~146 ms, against 472–546 ms for
the base model writing its native JSON.

## Training

LoRA r 16, alpha 32, on the language model's attention and MLP projections. Warm-started from the
computer-use run (E) and trained further on a mix of AGUVIS computer-use steps (5,000), Wave-UI element
clicks (1,000) and boxes (4,000), RefCOCO photo boxes (5,000) and RefCOCO masks (6,000). One RTX PRO 6000,
about 4 hours.

## Limits

- One step at a time from the screenshot, the instruction and the previous actions: no memory and no
  acting. Text arguments (what to type, the final answer) should come from the base model: the merged
  weights cannot switch the adapter off, so for agents that type, use the LoRA on the base model instead.
- Trained on AGUVIS, Wave-UI and RefCOCO; check their terms for your use.
- Points can miss small targets; boxes on UI elements are weaker (mean IoU 0.467 on ScreenSpot) than on
  photos.

## Citation

```bibtex
@article{umapathi2026slotreading,
  title   = "Stop Decoding Coordinates: 3.5× Faster Computer-Use Grounding",
  author  = "Umapathi, Logesh Kumar",
  journal = "logeshumapathi.com",
  year    = "2026",
  month   = "Oct",
  url     = "https://logeshumapathi.com/blog/2026/10/06/slot-reading.html"
}
```