Spaces:
Running on Zero
Running on Zero
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
|