Spaces:
Running on Zero
Running on Zero
Download locus/models/draft_verify.py from hugging-apps/locus-2b-grounding: direct link, hf CLI and curl.
- Browser
- Download file 8.83 kB
-
https://huggingface.co/spaces/hugging-apps/locus-2b-grounding/resolve/main/locus/models/draft_verify.py
- Command line
-
hf download hf://spaces/hugging-apps/locus-2b-grounding/locus/models/draft_verify.py
-
curl -L -o draft_verify.py https://huggingface.co/spaces/hugging-apps/locus-2b-grounding/resolve/main/locus/models/draft_verify.py
8.83 kB
| """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 | |
| 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 | |