SudharshanR
ElideDB query by example: no text, no model at query time
a1dd5ba
Raw
History Blame Contribute Delete
6.88 kB
"""ITM cross-encoder rerank — the discarded head, wired for live queries.
InternVideo2's checkpoint ships an `itm_head` that every load discarded
("Some weights ... were not used"). It is the cross-encoder half of the
model's own retrieval recipe (BLIP-2 arXiv 2301.12597): text tokens
cross-attend to video tokens, a 2-class head reads the fused CLS. It
out-ranks everything else in this store — measured at k=1.5xsupport,
ITM alone 0.38/0.25 against the cosine ensemble's 0.32/0.21 and the
shipped RRF path's 0.29/0.23.
Two measurements decide the shape of this module:
NO POOLING. Vision tokens are 1,025 x 1,408 per episode = 3.24 GB
corpus-wide, four times the raw source, so storing them in the store
is out. Pooling them is worse than out: 16x16 -> 8x8 per frame drops
Spearman against the full-token score to 0.18, and to NEGATIVE on two
of three probe queries. Cross-attention does not tolerate a reduced
key set. So the tokens are a disposable CACHE outside the store's
byte budget, never a table.
CASCADE, NOT SCAN. Reranking the top-N of the cheap fused ranking
reaches the full-scan number exactly: N=500 gives 0.40/0.27, N=150
already gives 0.38/0.25, and every query with support under ~200 is
saturated at N=150. So N scales with the ceiling k rather than the
corpus, which is L7 ("ITM is evidence inside a candidate set, not
authority over the corpus") arrived at from the cost side.
ITM IS A STAGE, NOT A CHANNEL — and that distinction was measured, not
chosen. Making it a weighted RRF voter and refitting cost 0.38 -> 0.27
for two compounding reasons: RRF converts every channel to RANKS, which
throws away the logit margin's scale, and that scale is where the
cross-encoder's separation lives (the 0.40 offline number came from
adding z-scores, not ranks); and the k-ladder refit then optimized the
ladder mean away from the 1.5x-support operating point the product
metric uses. So ITM stays a rerank STAGE alongside NMS and the
confidence cut - stages the fitter switches on and off rather than
weights - and the fit models it as such.
The cache is content-addressed by (episode, model) and capped; deleting
it costs time, never correctness.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
import numpy as np
_S: dict = {}
# 3.24 GB holds the whole 1,122-episode corpus. This is a DISPOSABLE
# cache under the store's _cache/, not a table: it never enters the
# store's byte ledger, and deleting it costs recompute time, never
# correctness. Override with ELIDEDB_ITM_CACHE_GB.
CACHE_CAP_GB = float(__import__("os").environ.get(
"ELIDEDB_ITM_CACHE_GB", "4.0"))
NF = 4
def _head():
"""The checkpoint's own 2-class matcher — AutoModel drops it because
the vendored class never declares the attribute."""
if "head" in _S:
return _S["head"], _S["m"], _S["dev"]
import torch
from .iv2 import MDIR, load_model
sd = torch.load(f"{MDIR}/pytorch_model.bin", map_location="cpu",
weights_only=True, mmap=True)
m, dev = load_model()
h = torch.nn.Linear(sd["itm_head.weight"].shape[1], 2)
h.load_state_dict({"weight": sd["itm_head.weight"],
"bias": sd["itm_head.bias"]})
h = h.to(dev, m.dtype).eval()
_S["head"], _S["m"], _S["dev"] = h, m, dev
return h, m, dev
def _cache_dir(store):
d = Path(store.dir) / "_cache" / "itm_tokens"
d.mkdir(parents=True, exist_ok=True)
return d
def _cache_path(store, stream, ts):
key = hashlib.sha1(f"{stream}|{ts}|iv2-1b|{NF}".encode()).hexdigest()
return _cache_dir(store) / f"{key}.npy"
def _evict(store):
"""Keep the disposable cache under CACHE_CAP_GB, oldest first."""
d = _cache_dir(store)
files = sorted(d.glob("*.npy"), key=lambda p: p.stat().st_mtime)
total = sum(p.stat().st_size for p in files)
cap = CACHE_CAP_GB * 1e9
while total > cap and files:
p = files.pop(0)
total -= p.stat().st_size
try:
p.unlink()
except OSError:
pass
def _vision_tokens(store, frames_tbl, key):
"""Cached (1, T, C) vision tokens for one episode."""
import cv2
import torch
from .iv2 import V_MEAN, V_STD
from .video import FrameSet
s, a, b = key
p = _cache_path(store, s, a)
_, m, dev = _head()
if p.exists():
arr = np.load(p)
return torch.from_numpy(arr).to(dev, m.dtype)
import pyarrow.compute as pc
sel = frames_tbl.filter(pc.and_(
pc.equal(frames_tbl.column("stream"), s),
pc.and_(pc.greater_equal(frames_tbl.column("ts"), a),
pc.less_equal(frames_tbl.column("ts"), b))))
if len(sel) < 2:
return None
pi = np.linspace(0, len(sel) - 1, min(NF, len(sel))).round().astype(int)
try:
dec = FrameSet(store, "frames", sel.take(pi)).decode(width=224)
except Exception:
return None
fr = [f for _, f in sorted(dec)]
if len(fr) < 2:
return None
fs = [cv2.resize(f, (224, 224)) for f in fr]
x = (np.stack(fs).astype(np.float32) / 255.0 - V_MEAN) / V_STD
px = torch.from_numpy(x).permute(0, 3, 1, 2)[None].to(dev, m.dtype)
with torch.no_grad():
vis, _ = m.encode_vision(px, test=True)
np.save(p, vis.cpu().numpy())
_evict(store)
return vis
def itm_scores(store, text, keys):
"""Logit margin P(match) - P(no match) for each candidate episode.
Returns np.array aligned with `keys`; NaN where frames are
undecodable. The vision pass dominates and is query-independent, so
a repeated query over the same candidates is nearly free."""
import torch
head, m, dev = _head()
frames_tbl = store.table("frames").scan()
tok = m.tokenizer(text, padding="max_length", truncation=True,
max_length=m._config.max_txt_l,
return_tensors="pt").to(dev)
out = np.full(len(keys), np.nan, np.float32)
for i, k in enumerate(keys):
vis = _vision_tokens(store, frames_tbl, k)
if vis is None:
continue
with torch.no_grad():
vam = torch.ones(vis.shape[:2], dtype=torch.long, device=dev)
o = m.get_text_encoder()(
tok.input_ids, attention_mask=tok.attention_mask,
encoder_hidden_states=vis, encoder_attention_mask=vam,
return_dict=True, mode="multi_modal")
lg = head(o.last_hidden_state[:, 0]).float()[0]
out[i] = float(lg[1] - lg[0])
return out
def rerank_depth(k_max, n_total):
"""Candidates to rerank. Measured: N=150 saturates every query with
support under ~200; only the 247-support query needed 500. Scaling
with the ceiling rather than the corpus keeps the cost proportional
to what the caller actually asked for."""
return int(min(max(150, 2 * k_max), n_total))