ruotian commited on
Commit
4a027f2
·
verified ·
1 Parent(s): a3043b9

Add Self-Contrastive Grounding inference and ablations

Browse files

Adds the six-prefill cache-reuse implementation, evaluator flag, full three-benchmark metrics, ablations, and reproducibility manifest. Model weights are unchanged.

Files changed (4) hide show
  1. README.md +49 -0
  2. evaluate.py +31 -8
  3. self_contrast.py +328 -0
  4. self_contrast_manifest.json +81 -0
README.md CHANGED
@@ -36,6 +36,55 @@ element-grounding subsets. OSWorld-G uses its 510 target-bearing examples;
36
  refusal-only rows are excluded. These public benchmarks were used during model
37
  selection, so results are test-tuned rather than held-out validation estimates.
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  ## Direct inference
40
 
41
  The repository includes the exact loader and evaluator. `visual_merger.pt` must
 
36
  refusal-only rows are excluded. These public benchmarks were used during model
37
  selection, so results are test-tuned rather than held-out validation estimates.
38
 
39
+ ## Self-Contrastive Grounding
40
+
41
+ The release also includes Self-Contrastive Grounding, a training-free extension
42
+ of the paper's contrast-mining principle. Training mines observed hard
43
+ distractors from disagreement between models. At inference, Self-Contrast mines
44
+ latent distractors from disagreement between deterministic views of the same
45
+ model, then asks every other view to verify each visible coordinate. A proposal
46
+ is never scored by the view that generated it, which prevents self-confirmation.
47
+
48
+ | Inference | ScreenSpot-Pro | UI-Vision | OSWorld-G |
49
+ |---|---:|---:|---:|
50
+ | Direct | 65.09 | 37.12 | 69.41 |
51
+ | Self-Contrast | **71.16** | **44.09** | **72.75** |
52
+
53
+ The method uses one full-screen view, one 40% incumbent-centered revisit, and
54
+ four fixed overlapping 60% views. All crops are enlarged by 2x. Within each
55
+ view, coordinate-string mean token log-likelihoods are standardized; evidence
56
+ is averaged across non-source views and combined at equal weight with proximity
57
+ to the incumbent revisit. This one configuration is shared by all three
58
+ benchmarks: there is no benchmark-specific gate, router, prompt, or threshold.
59
+
60
+ The implementation retains each view's visual prefix after greedy candidate
61
+ generation and reuses its KV cache for batched coordinate scoring. It therefore
62
+ uses six visual prefills, rather than the twelve prefills of a naive
63
+ generate-then-rescore implementation, and requires no weight update.
64
+
65
+ ```bash
66
+ python evaluate.py \
67
+ --model ruotian/SelectGround-8B \
68
+ --benchmark screenspot_pro \
69
+ --data data/screenspot-pro \
70
+ --self-contrast \
71
+ --output outputs/screenspot-pro-self-contrast.jsonl
72
+ ```
73
+
74
+ Full 8B ablations, using the same benchmark protocols, are:
75
+
76
+ | Variant | ScreenSpot-Pro | UI-Vision | OSWorld-G |
77
+ |---|---:|---:|---:|
78
+ | Full | 71.16 | 44.09 | 72.75 |
79
+ | no latent distractors | 71.22 | 43.44 | 69.61 |
80
+ | one latent distractor | 70.97 | 43.44 | 70.59 |
81
+ | no recurrent anchor | 68.82 | 43.23 | 72.75 |
82
+ | no cross-view evidence | 71.16 | 43.46 | 69.41 |
83
+ | no anchor proximity | 70.15 | 44.10 | 72.94 |
84
+
85
+ `self_contrast_manifest.json` records the exact protocol, full counts, split
86
+ metrics, artifact checksums, and cache-reuse equivalence test.
87
+
88
  ## Direct inference
89
 
90
  The repository includes the exact loader and evaluator. `visual_merger.pt` must
evaluate.py CHANGED
@@ -7,6 +7,7 @@ from pathlib import Path
7
  from PIL import Image
8
 
9
  from selectground import SelectGround
 
10
 
11
 
12
  def load_cases(name: str, root: Path):
@@ -117,6 +118,19 @@ parser.add_argument("--benchmark", choices=("screenspot_pro", "ui_vision", "oswo
117
  parser.add_argument("--data", type=Path, required=True)
118
  parser.add_argument("--output", type=Path, required=True)
119
  parser.add_argument("--lcr", action="store_true")
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  parser.add_argument(
121
  "--lcr-variant",
122
  choices=("full", "no_competitor", "one_competitor", "no_incumbent"),
@@ -126,6 +140,8 @@ parser.add_argument("--limit", type=int)
126
  parser.add_argument("--num-shards", type=int, default=1)
127
  parser.add_argument("--shard", type=int, default=0)
128
  args = parser.parse_args()
 
 
129
 
130
  cases = (
131
  case for index, case in enumerate(load_cases(args.benchmark, args.data))
@@ -137,19 +153,26 @@ existing = []
137
  if args.output.exists():
138
  existing = [json.loads(line) for line in args.output.read_text().splitlines() if line.strip()]
139
  done = {row["id"] for row in existing}
140
- grounder = SelectGround(args.model)
141
  args.output.parent.mkdir(parents=True, exist_ok=True)
142
  with args.output.open("a") as output:
143
  for number, case in enumerate(cases, 1):
144
  if case["id"] in done:
145
  continue
146
- prediction = grounder.predict(
147
- case["image"],
148
- case["instruction"],
149
- lcr=args.lcr,
150
- benchmark=args.benchmark,
151
- lcr_variant=args.lcr_variant,
152
- )
 
 
 
 
 
 
 
153
  row = {
154
  "id": case["id"],
155
  "instruction": case["instruction"],
 
7
  from PIL import Image
8
 
9
  from selectground import SelectGround
10
+ from self_contrast import SelfContrastGrounder
11
 
12
 
13
  def load_cases(name: str, root: Path):
 
118
  parser.add_argument("--data", type=Path, required=True)
119
  parser.add_argument("--output", type=Path, required=True)
120
  parser.add_argument("--lcr", action="store_true")
121
+ parser.add_argument("--self-contrast", action="store_true")
122
+ parser.add_argument(
123
+ "--self-contrast-variant",
124
+ choices=(
125
+ "full",
126
+ "no_latent_distractors",
127
+ "one_latent_distractor",
128
+ "no_recurrent_anchor",
129
+ "no_cross_view_evidence",
130
+ "no_anchor_proximity",
131
+ ),
132
+ default="full",
133
+ )
134
  parser.add_argument(
135
  "--lcr-variant",
136
  choices=("full", "no_competitor", "one_competitor", "no_incumbent"),
 
140
  parser.add_argument("--num-shards", type=int, default=1)
141
  parser.add_argument("--shard", type=int, default=0)
142
  args = parser.parse_args()
143
+ if args.lcr and args.self_contrast:
144
+ parser.error("--lcr and --self-contrast are mutually exclusive")
145
 
146
  cases = (
147
  case for index, case in enumerate(load_cases(args.benchmark, args.data))
 
153
  if args.output.exists():
154
  existing = [json.loads(line) for line in args.output.read_text().splitlines() if line.strip()]
155
  done = {row["id"] for row in existing}
156
+ grounder = SelfContrastGrounder(args.model) if args.self_contrast else SelectGround(args.model)
157
  args.output.parent.mkdir(parents=True, exist_ok=True)
158
  with args.output.open("a") as output:
159
  for number, case in enumerate(cases, 1):
160
  if case["id"] in done:
161
  continue
162
+ if args.self_contrast:
163
+ prediction = grounder.predict(
164
+ case["image"],
165
+ case["instruction"],
166
+ variant=args.self_contrast_variant,
167
+ )
168
+ else:
169
+ prediction = grounder.predict(
170
+ case["image"],
171
+ case["instruction"],
172
+ lcr=args.lcr,
173
+ benchmark=args.benchmark,
174
+ lcr_variant=args.lcr_variant,
175
+ )
176
  row = {
177
  "id": case["id"],
178
  "instruction": case["instruction"],
self_contrast.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ import math
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from PIL import Image
10
+ import torch
11
+ from transformers.cache_utils import DynamicCache
12
+
13
+ from selectground import SelectGround, _map_crop, _prediction
14
+
15
+
16
+ GRID_CENTERS = ((0.3, 0.3), (0.7, 0.3), (0.3, 0.7), (0.7, 0.7))
17
+
18
+
19
+ @dataclass
20
+ class _Prefix:
21
+ cache: DynamicCache
22
+ logits: torch.Tensor
23
+ position_ids: torch.Tensor
24
+ attention_mask: torch.Tensor
25
+ length: int
26
+
27
+
28
+ class SelfContrastGrounder:
29
+ """Training-free self-contrastive grounding with six visual prefills."""
30
+
31
+ def __init__(self, checkpoint: str = "ruotian/SelectGround-8B") -> None:
32
+ self.grounder = SelectGround(checkpoint)
33
+
34
+ def predict(
35
+ self,
36
+ image: str | Path | Image.Image,
37
+ instruction: str,
38
+ *,
39
+ variant: str = "full",
40
+ ) -> dict[str, Any]:
41
+ variants = {
42
+ "full": (GRID_CENTERS, True, True, 1.0),
43
+ "no_latent_distractors": ((), True, True, 1.0),
44
+ "one_latent_distractor": (GRID_CENTERS[:1], True, True, 1.0),
45
+ "no_recurrent_anchor": (GRID_CENTERS, False, True, 1.0),
46
+ "no_cross_view_evidence": (GRID_CENTERS, True, False, 1.0),
47
+ "no_anchor_proximity": (GRID_CENTERS, True, True, 0.0),
48
+ }
49
+ if variant not in variants:
50
+ raise ValueError(f"unknown self-contrast variant: {variant}")
51
+ grid_centers, use_anchor, use_evidence, proximity_weight = variants[variant]
52
+ source = (
53
+ Image.open(image).convert("RGB")
54
+ if not isinstance(image, Image.Image)
55
+ else image.convert("RGB")
56
+ )
57
+ full_box = (0, 0, source.width, source.height)
58
+ views: dict[str, tuple[tuple[int, int, int, int], Image.Image]] = {
59
+ "full": (full_box, source)
60
+ }
61
+ candidates = []
62
+ prefixes = {}
63
+
64
+ p0, prefixes["full"] = self._observe(source, instruction)
65
+ candidates.append(self._candidate("p0", p0, full_box, source.size))
66
+ if use_anchor and p0["point"] is not None:
67
+ box = _crop_box(tuple(p0["point"]), source.size, 0.40)
68
+ views["q0"] = (box, _view(source, box))
69
+ for index, center in enumerate(grid_centers):
70
+ point = (center[0] * source.width, center[1] * source.height)
71
+ box = _crop_box(point, source.size, 0.60)
72
+ views[f"grid_{index}"] = (box, _view(source, box))
73
+
74
+ for name, (box, view) in tuple(views.items())[1:]:
75
+ prediction, prefixes[name] = self._observe(view, instruction)
76
+ candidates.append(self._candidate(name, prediction, box, source.size))
77
+
78
+ evidence = {}
79
+ for view_name, (box, _) in views.items():
80
+ visible = {
81
+ candidate["name"]: response
82
+ for candidate in candidates
83
+ if candidate["point"] is not None
84
+ and (response := _response(candidate["point"], box)) is not None
85
+ }
86
+ scores = self._score(prefixes.pop(view_name), list(visible.values()))
87
+ evidence[view_name] = {
88
+ name: {"response": response, **scores[response]}
89
+ for name, response in visible.items()
90
+ }
91
+ selected = _select(
92
+ candidates,
93
+ evidence,
94
+ source.size,
95
+ proximity_weight=proximity_weight,
96
+ use_evidence=use_evidence,
97
+ )
98
+ point = selected["point"]
99
+ normalized = (
100
+ [1000 * point[0] / source.width, 1000 * point[1] / source.height]
101
+ if point is not None
102
+ else None
103
+ )
104
+ return {
105
+ "method": "SelectGround+SelfContrast",
106
+ "variant": variant,
107
+ "point": point,
108
+ "normalized_point": normalized,
109
+ "raw_response": selected["raw_response"],
110
+ "selected_candidate": selected["name"],
111
+ }
112
+
113
+ def _candidate(
114
+ self,
115
+ name: str,
116
+ prediction: dict[str, Any],
117
+ box: tuple[int, int, int, int],
118
+ source_size: tuple[int, int],
119
+ ) -> dict[str, Any]:
120
+ mapped = (
121
+ prediction
122
+ if name == "p0" or prediction["point"] is None
123
+ else _map_crop(prediction, box, source_size, 2.0)
124
+ )
125
+ return {
126
+ "name": name,
127
+ "point": mapped["point"],
128
+ "source_view": "full" if name == "p0" else name,
129
+ "raw_response": prediction["raw_response"],
130
+ }
131
+
132
+ @torch.inference_mode()
133
+ def _observe(
134
+ self, image: Image.Image, instruction: str
135
+ ) -> tuple[dict[str, Any], _Prefix]:
136
+ inputs = self.grounder._inputs(image, instruction, False)
137
+ input_ids = inputs["input_ids"]
138
+ length = int(input_ids.shape[1])
139
+ position_ids, _ = self.grounder.core.get_rope_index(
140
+ input_ids,
141
+ inputs.get("image_grid_thw"),
142
+ inputs.get("video_grid_thw"),
143
+ attention_mask=inputs.get("attention_mask"),
144
+ )
145
+ cache = DynamicCache(config=self.grounder.core.language_model.config)
146
+ output = self.grounder.model(
147
+ **inputs,
148
+ past_key_values=cache,
149
+ position_ids=position_ids,
150
+ cache_position=torch.arange(length, device=self.grounder.device),
151
+ use_cache=True,
152
+ logits_to_keep=1,
153
+ )
154
+ logits = output.logits[:, -1, :].detach()
155
+ raw = self.grounder._decode(
156
+ logits, cache, position_ids[:, :, -1:] + 1, None
157
+ )
158
+ cache.crop(length)
159
+ if cache.get_seq_length() != length:
160
+ raise RuntimeError("could not restore the visual prefix after decoding")
161
+ return (
162
+ _prediction(raw, image.size, integer=False),
163
+ _Prefix(cache, logits, position_ids, inputs["attention_mask"], length),
164
+ )
165
+
166
+ @torch.inference_mode()
167
+ def _score(
168
+ self, prefix: _Prefix, responses: list[str]
169
+ ) -> dict[str, dict[str, float | int]]:
170
+ unique = list(dict.fromkeys(responses))
171
+ if not unique:
172
+ return {}
173
+ encoded = [_token_ids(self.grounder, response) for response in unique]
174
+ first = torch.log_softmax(prefix.logits.float(), -1)
175
+ logps = [[float(first[0, values[0]])] for values in encoded]
176
+ maximum = max(map(len, encoded))
177
+ if maximum > 1:
178
+ tokenizer = self.grounder.processor.tokenizer
179
+ pad = tokenizer.pad_token_id or tokenizer.eos_token_id
180
+ continuation = torch.full(
181
+ (len(encoded), maximum - 1),
182
+ int(pad),
183
+ dtype=torch.long,
184
+ device=self.grounder.device,
185
+ )
186
+ mask = torch.zeros_like(continuation, dtype=torch.bool)
187
+ for index, values in enumerate(encoded):
188
+ if len(values) > 1:
189
+ continuation[index, : len(values) - 1] = torch.tensor(
190
+ values[:-1], device=self.grounder.device
191
+ )
192
+ mask[index, : len(values) - 1] = True
193
+ prefix.cache.batch_repeat_interleave(len(encoded))
194
+ positions = prefix.position_ids.repeat_interleave(len(encoded), dim=-2)
195
+ offsets = torch.arange(maximum - 1, device=self.grounder.device).view(
196
+ *([1] * (positions.ndim - 1)), -1
197
+ )
198
+ output = self.grounder.model(
199
+ input_ids=continuation,
200
+ past_key_values=prefix.cache,
201
+ attention_mask=torch.cat(
202
+ (prefix.attention_mask.repeat(len(encoded), 1), mask.long()), 1
203
+ ),
204
+ position_ids=positions[..., -1:] + 1 + offsets,
205
+ cache_position=torch.arange(
206
+ prefix.length,
207
+ prefix.length + maximum - 1,
208
+ device=self.grounder.device,
209
+ ),
210
+ use_cache=True,
211
+ )
212
+ for index, values in enumerate(encoded):
213
+ if len(values) <= 1:
214
+ continue
215
+ logits = output.logits[index, : len(values) - 1].float()
216
+ labels = torch.tensor(values[1:], device=self.grounder.device)
217
+ selected = torch.log_softmax(logits, -1).gather(1, labels[:, None])[:, 0]
218
+ logps[index].extend(float(value) for value in selected)
219
+ return {
220
+ response: {
221
+ "token_count": len(values),
222
+ "mean_logprob": sum(values_logps) / len(values),
223
+ }
224
+ for response, values, values_logps in zip(unique, encoded, logps, strict=True)
225
+ }
226
+
227
+
228
+ def _crop_box(
229
+ point: tuple[float, float], size: tuple[int, int], fraction: float
230
+ ) -> tuple[int, int, int, int]:
231
+ width, height = size
232
+ crop_width = min(width, max(320, round(fraction * width)))
233
+ crop_height = min(height, max(320, round(fraction * height)))
234
+ left = round(min(max(0.0, point[0] - crop_width / 2), width - crop_width))
235
+ top = round(min(max(0.0, point[1] - crop_height / 2), height - crop_height))
236
+ return left, top, left + crop_width, top + crop_height
237
+
238
+
239
+ def _view(source: Image.Image, box: tuple[int, int, int, int]) -> Image.Image:
240
+ crop = source.crop(box)
241
+ return crop.resize(
242
+ (2 * crop.width, 2 * crop.height), Image.Resampling.LANCZOS
243
+ )
244
+
245
+
246
+ def _response(point: list[float], box: tuple[int, int, int, int]) -> str | None:
247
+ left, top, right, bottom = box
248
+ x, y = map(float, point)
249
+ if not left <= x < right or not top <= y < bottom:
250
+ return None
251
+ return (
252
+ f"[{round(1000 * (x - left) / (right - left))},"
253
+ f"{round(1000 * (y - top) / (bottom - top))}]"
254
+ )
255
+
256
+
257
+ def _token_ids(grounder: SelectGround, response: str) -> list[int]:
258
+ values = grounder.processor.tokenizer(
259
+ response, add_special_tokens=False
260
+ )["input_ids"]
261
+ if values and isinstance(values[0], list):
262
+ values = values[0]
263
+ result = [int(value) for value in values]
264
+ if not result:
265
+ raise ValueError(f"empty tokenization for {response!r}")
266
+ return result
267
+
268
+
269
+ def _zscore(values: list[float]) -> list[float]:
270
+ mean = sum(values) / len(values)
271
+ std = math.sqrt(sum((value - mean) ** 2 for value in values) / len(values))
272
+ return [(value - mean) / max(std, 1e-6) for value in values]
273
+
274
+
275
+ def _select(
276
+ candidates: list[dict[str, Any]],
277
+ evidence: dict[str, dict[str, dict[str, Any]]],
278
+ size: tuple[int, int],
279
+ *,
280
+ proximity_weight: float,
281
+ use_evidence: bool,
282
+ ) -> dict[str, Any]:
283
+ eligible = [candidate for candidate in candidates if candidate["point"] is not None]
284
+ if not eligible:
285
+ return candidates[0]
286
+ by_name = {candidate["name"]: candidate for candidate in eligible}
287
+ accumulated = defaultdict(list)
288
+ for view_name, values in evidence.items():
289
+ unique = {}
290
+ for name, value in values.items():
291
+ if name in by_name:
292
+ unique.setdefault(value["response"], float(value["mean_logprob"]))
293
+ if not unique:
294
+ continue
295
+ normalized = dict(zip(unique, _zscore(list(unique.values())), strict=True))
296
+ for name, value in values.items():
297
+ if name in by_name and by_name[name]["source_view"] != view_name:
298
+ accumulated[name].append(normalized[value["response"]])
299
+ if use_evidence:
300
+ eligible = [candidate for candidate in eligible if accumulated[candidate["name"]]]
301
+ if not eligible:
302
+ return candidates[0]
303
+ likelihood = _zscore(
304
+ [
305
+ sum(accumulated[candidate["name"]])
306
+ / len(accumulated[candidate["name"]])
307
+ for candidate in eligible
308
+ ]
309
+ )
310
+ else:
311
+ likelihood = [0.0] * len(eligible)
312
+ by_name = {candidate["name"]: candidate for candidate in eligible}
313
+ anchor = by_name.get("q0", by_name.get("p0", eligible[0]))["point"]
314
+ width, height = size
315
+ proximity = _zscore(
316
+ [
317
+ -math.hypot(
318
+ (candidate["point"][0] - anchor[0]) / width,
319
+ (candidate["point"][1] - anchor[1]) / height,
320
+ )
321
+ for candidate in eligible
322
+ ]
323
+ )
324
+ scores = [
325
+ likelihood[index] + proximity_weight * proximity[index]
326
+ for index in range(len(eligible))
327
+ ]
328
+ return eligible[max(range(len(eligible)), key=lambda index: (scores[index], -index))]
self_contrast_manifest.json ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": "selectground.self_contrast.v1",
3
+ "method": "Self-Contrastive Grounding",
4
+ "training_free": true,
5
+ "model": {
6
+ "repo": "ruotian/SelectGround-8B",
7
+ "weight_source_revision": "a3043b9d3bcd00779bbf2f293f18dd5651e433cc",
8
+ "plain_base_start": true,
9
+ "aggregate": false,
10
+ "training_manifest_sha256": "f0352e027ffc99b69fc93c3960163702742abe6f8aa2d5999284ad717f21bb87"
11
+ },
12
+ "protocol": {
13
+ "prompt": "direct SelectGround prompt",
14
+ "decoding": "greedy",
15
+ "max_new_tokens": 32,
16
+ "views": [
17
+ {"name": "full", "crop_fraction": 1.0, "scale": 1.0},
18
+ {"name": "q0", "center": "initial prediction", "crop_fraction": 0.4, "scale": 2.0},
19
+ {"name": "grid_0", "center": [0.3, 0.3], "crop_fraction": 0.6, "scale": 2.0},
20
+ {"name": "grid_1", "center": [0.7, 0.3], "crop_fraction": 0.6, "scale": 2.0},
21
+ {"name": "grid_2", "center": [0.3, 0.7], "crop_fraction": 0.6, "scale": 2.0},
22
+ {"name": "grid_3", "center": [0.7, 0.7], "crop_fraction": 0.6, "scale": 2.0}
23
+ ],
24
+ "coordinate_evidence": "mean token log probability",
25
+ "within_view_normalization": "population z-score over unique visible coordinate strings",
26
+ "cross_view_aggregation": "mean over visible non-source views, then population z-score over candidates",
27
+ "source_view_excluded": true,
28
+ "anchor": "q0, falling back to p0",
29
+ "proximity_weight": 1.0,
30
+ "p0_prior": 0.0,
31
+ "parameter_scope": "one shared configuration for all benchmarks",
32
+ "visual_prefills": 6,
33
+ "visual_prefix_cache_reused": true,
34
+ "additional_training": false,
35
+ "router_or_gate": false
36
+ },
37
+ "evaluation": {
38
+ "test_tuned": true,
39
+ "screenspot_pro": {"total": 1581, "correct": 1125, "accuracy_pct": 71.15749525616698},
40
+ "ui_vision": {
41
+ "total": 5479,
42
+ "correct_micro": 2398,
43
+ "accuracy_macro_pct": 44.09020207146093,
44
+ "splits": {
45
+ "basic": {"total": 1772, "correct": 888, "accuracy_pct": 50.112866817155755},
46
+ "functional": {"total": 1772, "correct": 867, "accuracy_pct": 48.92776523702032},
47
+ "spatial": {"total": 1935, "correct": 643, "accuracy_pct": 33.229974160206716}
48
+ }
49
+ },
50
+ "osworld_g": {"total": 510, "correct": 371, "accuracy_pct": 72.74509803921569}
51
+ },
52
+ "ablation_accuracy_pct": {
53
+ "full": {"screenspot_pro": 71.15749525616698, "ui_vision": 44.09020207146093, "osworld_g": 72.74509803921569},
54
+ "no_latent_distractors": {"screenspot_pro": 71.22074636306135, "ui_vision": 43.43973534140997, "osworld_g": 69.6078431372549},
55
+ "one_latent_distractor": {"screenspot_pro": 70.96774193548387, "ui_vision": 43.43677027859925, "osworld_g": 70.58823529411765},
56
+ "no_recurrent_anchor": {"screenspot_pro": 68.81720430107528, "ui_vision": 43.22846732500783, "osworld_g": 72.74509803921569},
57
+ "no_cross_view_evidence": {"screenspot_pro": 71.15749525616698, "ui_vision": 43.45696187026441, "osworld_g": 69.41176470588235},
58
+ "no_anchor_proximity": {"screenspot_pro": 70.14547754585705, "ui_vision": 44.09516004534115, "osworld_g": 72.94117647058823}
59
+ },
60
+ "artifacts": {
61
+ "metrics_sha256": "33549379cfb984f200b6b3d1e464a89ac872c3dc439b28bdc720a60b542a596c",
62
+ "predictions_sha256": "f5214508ff44c79b0e9aa5e343b18f7d05dc2929a0b4ff23dfbf51c1b3def304",
63
+ "raw_evidence_sha256": {
64
+ "screenspot_pro": "cef8b52eef5932a84f8e26d00958532dfc8bd12e21a3d45733bd2d850353629c",
65
+ "ui_vision": "a52470cc7ca2f79b2d2d6b1f6fecf9df45b0505248fd1776dc446dad7900ebca",
66
+ "osworld_g": "b1edf2c473795828fa8de67558dfe19dbc9ee1295d42310dbc5dbf74fb43b4fc"
67
+ },
68
+ "raw_evidence_runner_sha256": "21e6392a8b36bc8b78f1222fa6f4b89e9bea2a35ed75e15dc2a388b147e9f6fe",
69
+ "optimized_eval_runner_sha256": "8671248c9f015aa2046b5fe238259f61c0dba74d1a69c52b9c00b3d89bb31e29",
70
+ "scorer_sha256": "773aba1d2017da538e03c6436b3a2bb948f6714179019ab0a502c2769ac15000",
71
+ "release_runner_sha256": "56fab886aca1691c18de7b483ac9883e932dcb96b09cffe153c1fb2e25624ad5",
72
+ "release_evaluator_sha256": "a8a4993273a098a578895c53beda15210f74aa118edb95f09aea028fc92ea283"
73
+ },
74
+ "optimized_equivalence_check": {
75
+ "shared_cases": 125,
76
+ "candidate_generation_exact": 125,
77
+ "cross_view_evidence_exact": 125,
78
+ "hardware": "NVIDIA RTX A6000",
79
+ "note": "The six-prefill cache-reuse implementation was exactly equal to the twelve-prefill prototype on every shared case."
80
+ }
81
+ }