File size: 8,834 Bytes
a99b8df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Box head as a speculative drafter for coordinate tokens (Jacobi verification over a shared prefix).

The box head answers every question of an image in one branch pass over the cached image prefix. Its boxes are
rendered as the backbone's own coordinate-token answer ("x0, y0, x1, y1]" in 0–1000 units, after a teacher-forced
JSON preamble) and verified in parallel by the backbone's LM head, with the adapter disabled (base model):

  pass k:  argmax g_t of every draft position given the draft before it, all questions batched as branches.
           Accept the longest prefix with g_t == draft_t; the token after it, g_n, is exact too.
           Next draft = accepted + g_n + g_{n+1..} (Jacobi iterate). Stop when nothing changes.

The fixed point is exactly the greedy decode under the same computation. Rows still unconverged after max_passes
finish with plain greedy decoding from their accepted prefix, so the output is always the greedy answer; the cost
is the number of sequential passes. max_passes=0 is plain greedy decoding (the coordinate-token control on the same
prompt, cache and kernels).

accept < 1 relaxes the check (typical acceptance, as in Medusa): a draft token also passes when the verifier gives it
at least accept x the probability of its argmax. The output is then no longer the greedy answer but the head's box
unless the verifier clearly disagrees; accept=1 is the exact rule above.
"""
from __future__ import annotations

import re

import numpy as np
import torch

from locus.models.boxvlm import BOX_TOKEN, BoxVLM

# Continues BoxVLM.prefix ("...Locate:"): the zero-shot grounding prompt of the Qwen harness, with a colon.
VERIFY_PROMPT = ' "{}" in the image and output its bbox coordinates in JSON format.'
PREAMBLE = '```json\n[\n\t{"bbox_2d": ['


class DraftVerify:
    def __init__(self, model: BoxVLM, max_passes: int = 4, max_tokens: int = 24, verifier_adapter: bool = False,
                 accept: float = 1.0):
        assert model.n_layers_run == model.n_layers_total, "the verifier needs the full LM (no early exit)"
        self.m, self.tok = model, model.ad.tok
        self.max_passes, self.max_tokens = max_passes, max_tokens
        self.close = self.tok.encode("]", add_special_tokens=False)[0]
        self.vsuffix = model.suffix.replace(BOX_TOKEN, "") + PREAMBLE
        self.verifier_adapter = verifier_adapter  # aLoRA trained with --ar-weight: adapter on verifier branches too
        self.log_accept = float(np.log(accept)) if accept < 1 else None
        self._closes = {}

    # -- token helpers ------------------------------------------------------
    def is_close(self, t: int) -> bool:
        if t not in self._closes:
            self._closes[t] = self.tok.decode([t]).lstrip().startswith("]")
        return self._closes[t]

    def cut(self, ids: list[int]) -> list[int]:
        """Truncate after the first token that closes the coordinate list; append "]" if there is none."""
        for i, t in enumerate(ids):
            if self.is_close(t):
                return ids[: i + 1]
        return ids + [self.close]

    def render(self, box) -> list[int]:
        return self.tok.encode(", ".join(str(int(round(min(max(float(v), 0.0), 1.0) * 1000))) for v in box) + "]",
                               add_special_tokens=False)

    def parse(self, ids: list[int]) -> np.ndarray:
        m = re.match(r"\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\]", self.tok.decode(ids))
        if m is None:
            return np.full(4, np.nan)
        x0, y0, x1, y1 = (min(int(v), 1000) / 1000 for v in m.groups())
        return np.array([min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1)])

    def verifier_inputs(self, texts, boxes=None):
        """Verifier-branch prompt tokens per question (continuing BoxVLM.prefix), and rendered answers if boxes given."""
        prompts = [self.tok.encode(VERIFY_PROMPT.format(t) + self.vsuffix, add_special_tokens=False) for t in texts]
        return prompts, None if boxes is None else [self.render(b) for b in boxes]

    # -- verifier passes ----------------------------------------------------
    def _greedy_at(self, state, prompts, drafts, rows, row_image):
        """One batched branch pass: argmax at every draft position (and the one after the last) for the given rows.
        With accept < 1, a draft token the verifier finds plausible enough is returned in place of the argmax."""
        seqs = [prompts[i] + drafts[i] for i in rows]
        L = max(map(len, seqs))
        ids = torch.full((len(seqs), L), self.tok.pad_token_id, dtype=torch.long)
        mask = torch.zeros(len(seqs), L, dtype=torch.long)
        for j, s in enumerate(seqs):
            ids[j, : len(s)], mask[j, : len(s)] = torch.tensor(s), 1
        dev = row_image.device
        if self.m.fast is not None:
            h = self.m.branch_pass(state, ids.to(dev), mask.to(dev), row_image[rows], keep_cache=True,
                                   adapter=self.verifier_adapter).last_hidden_state
        else:
            ctx = self.m.ad.model.disable_adapter() if self.m.lora_r and not self.verifier_adapter else torch.no_grad()
            with ctx:
                h = self.m.branch_pass(state, ids.to(dev), mask.to(dev), row_image[rows], keep_cache=True).last_hidden_state
        spans = [(j, len(prompts[i]) - 1, len(prompts[i]) + len(drafts[i])) for j, i in enumerate(rows)]
        sel = torch.cat([h[j, a:b] for j, a, b in spans])  # LM head only where needed: len(draft) + 1 per row
        logits = self.m.base.lm_head(sel).float()
        pred = logits.argmax(-1)
        if self.log_accept is not None:
            draft = torch.tensor([t for i in rows for t in drafts[i] + [-1]], device=pred.device)
            lp = logits.log_softmax(-1)
            ok = (draft >= 0) & (lp.gather(1, draft.clamp(min=0)[:, None])[:, 0] - lp.max(-1).values >= self.log_accept)
            pred = torch.where(ok, draft, pred)
        pred = pred.tolist()
        out, k = [], 0
        for _, a, b in spans:
            out.append(pred[k : k + b - a])
            k += b - a
        return out

    @torch.no_grad()
    def __call__(self, prefix_inputs, texts: list[str], row_image: torch.Tensor, verify: bool = True,
                 draft: np.ndarray | None = None) -> dict:
        """`draft` (boxes from another model's head) replaces this model's head as the drafter."""
        m = self.m
        state = m.prefill(prefix_inputs)
        bid, bmask = m.tokenize_branches(texts)
        dev = row_image.device
        head, conf = m.forward_shared(None, bid.to(dev), bmask.to(dev), row_image, state=state)
        head = head.float().cpu().numpy()
        res = dict(head=head, conf=torch.sigmoid(conf.float()).cpu().numpy())
        if not verify:
            return res
        assert m.alora or not m.lora_r or self.verifier_adapter, \
            "the base-model verifier needs a base prefix: use an aLoRA (or frozen) head, or a jointly trained adapter"
        prompts = self.verifier_inputs(texts)[0]
        R = len(texts)
        drafts = [self.render(b) for b in (head if draft is None else draft)] if self.max_passes else [[] for _ in range(R)]
        acc = [0] * R  # length of the greedy-exact prefix of each draft
        passes = np.zeros(R, int)
        active = list(range(R)) if self.max_passes else []
        for _ in range(self.max_passes):
            if not active:
                break
            g = self._greedy_at(state, prompts, drafts, active, row_image)
            nxt = []
            for i, gi in zip(active, g):
                passes[i] += 1
                d = drafts[i]
                n = next((k for k, (a, b) in enumerate(zip(d, gi)) if a != b and not (self.is_close(a) and self.is_close(b))), len(d))
                if n == len(d):  # fixed point: every draft token is the greedy token
                    acc[i] = len(d)
                    continue
                acc[i] = n + 1  # g_n is exact as well
                drafts[i] = self.cut(d[:n] + gi[n:])
                nxt.append(i)
            active = nxt
        # Unconverged rows (and max_passes=0): plain greedy decoding from the exact prefix.
        todo = [i for i in range(R) if acc[i] < len(drafts[i]) or not drafts[i]]
        for i in todo:
            drafts[i] = drafts[i][: acc[i]]
        while todo:
            g = self._greedy_at(state, prompts, [d for d in drafts], todo, row_image)
            nxt = []
            for i, gi in zip(todo, g):
                passes[i] += 1
                drafts[i] = drafts[i] + [gi[-1]]
                if not self.is_close(gi[-1]) and len(drafts[i]) < self.max_tokens:
                    nxt.append(i)
            todo = nxt
        res.update(box=np.stack([self.parse(d) for d in drafts]), passes=passes, tokens=np.array([len(d) for d in drafts]))
        return res