Yashp2003/vstream-code / vstream_real.py
Yashp2003's picture
download
raw
11.6 kB
"""Real-VLM micro reproduction of vStream (arXiv:2604.16587) on HF Jobs.
Scaled, honest reproduction of the core method on a *single small VLM*
(Qwen/Qwen2-VL-2B-Instruct) over a handful of images, to verify the
mechanism on REAL model internals rather than synthetic data:
1. Extract cross-attention A^{(l,h)}_{t,i} for generated tokens -> vision tokens.
2. Cluster vision tokens into K semantic regions via DINOv2 features
(paper uses DINOv3; DINOv2 is a faithful stand-in for the clustering step).
3. For each region R_k, ablate it (zero its vision tokens) and re-run the
forward pass; the span-level ablation effect is
Delta_k(S) = sum_t [log p(y_t|x,y<t) - log p(y_t|ablate(x,R_k),y<t)].
4. Feature f_{S,k} = mean-pooled cross-attention over span S and region R_k,
concatenated across all L*H (layer,head) -> L*H-dim vector.
5. Train a LINEAR estimator w (L*H params) to maximize Pearson between
predicted w.f and target Delta; evaluate LDS = Spearman over regions,
and measure the per-10-token latency of the linear estimator vs the
perturbation (re-forward) baseline.
This is a TOY SCALE run: ~N_IMAGES images, K regions, a 2B model. It does
NOT reproduce the paper's 4x 7-9B models x 5 categories x 2000 examples.
It verifies the pipeline and the direction/magnitude of the claims.
"""
import json, time, os, math
import torch
import numpy as np
import torchvision
import requests
from PIL import Image
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor, Qwen2VLImageProcessor
from transformers.models.qwen2_vl.modeling_qwen2_vl import Qwen2VLCausalLMOutputWithPast
from sklearn.cluster import AgglomerativeClustering
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
N_IMAGES = int(os.environ.get("N_IMAGES", "6"))
K_REGIONS = int(os.environ.get("K_REGIONS", "24"))
GEN_TOKENS = int(os.environ.get("GEN_TOKENS", "40"))
print(f"device={DEVICE} n_images={N_IMAGES} K={K_REGIONS} gen_tokens={GEN_TOKENS}")
# ---------- load small VLM ----------
model_id = "Qwen/Qwen2-VL-2B-Instruct"
processor = AutoProcessor.from_pretrained(model_id)
model = Qwen2VLForConditionalGeneration.from_pretrained(
model_id, torch_dtype=torch.float16, attn_implementation="eager", # eager -> attentions
).to(DEVICE).eval()
# number of layers / heads -> L*H parameters of the estimator
L = model.config.text_config.num_hidden_layers
H = model.config.text_config.num_attention_heads
D = L * H
print(f"[Claim 1] VLM has L={L} layers, H={H} heads -> estimator params L*H = {D}")
# ---------- load DINOv2 for region clustering (stand-in for DINOv3) ----------
try:
import timm
dino = timm.create_model("vit_small_patch14_dinov2.lvd142m", pretrained=True).to(DEVICE).eval()
dino_forward = lambda x: dino.forward_features(x) # (B,1+patches,d)
except Exception as e:
from torchvision.models import Dinov2_ViTS14_Weights
dino = torchvision.models.dinov2_vits14(weights=Dinov2_ViTS14_Weights.DEFAULT).to(DEVICE).eval()
dino_forward = lambda x: dino.forward_features(x)["x_norm_patchtokens"]
def get_image(idx):
"""Generate a synthetic but structured scene so DINOv2 clustering yields
meaningful semantic regions and the VLM can caption it. Fully offline to
avoid network flakiness; this is a TOY stand-in for real benchmark images."""
rng_i = np.random.default_rng(idx)
img = Image.new("RGB", (518, 518), (int(rng_i.integers(40,90)),)*3)
from PIL import ImageDraw
d = ImageDraw.Draw(img)
colors = [(200,40,40),(40,160,60),(40,80,210),(220,200,40),(200,60,200)]
for c in range(5):
x0,y0 = rng_i.integers(40,420,2)
sz = rng_i.integers(70,160)
d.ellipse([x0,y0,x0+sz,y0+sz], fill=colors[c%len(colors)])
d.rectangle([x0,y0+sz+20,x0+sz//2,y0+sz+60], fill=colors[(c+2)%len(colors)])
return img
# a few task-style questions (mirrors the paper's diverse prompts)
QUESTIONS = [
"What objects are in this image and how are they arranged?",
"Describe what you see and count the main items.",
"What is happening in this scene? Explain step by step.",
"What colors are the main objects and where are they located?",
"List the shapes present and describe their positions.",
]
IMAGE_TOKEN_ID = processor.tokenizer.convert_tokens_to_ids("<|image_pad|>")
@torch.inference_mode()
def generate_and_attn(image, question, n_tokens):
msgs = [{"role":"user","content":[
{"type":"image"},{"type":"text","text":question}]}]
txt = processor.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
inputs = processor(text=[txt], images=[image], return_tensors="pt").to(DEVICE, torch.float16)
out = model.generate(
**inputs, max_new_tokens=n_tokens, do_sample=False,
output_attentions=True, return_dict_in_generate=True,
)
seq = out.sequences[0]
# locate the contiguous block of vision tokens (<|image_pad|>) in `seq`
vp = (seq == IMAGE_TOKEN_ID).nonzero(as_tuple=True)[0]
if len(vp) == 0:
raise RuntimeError("no image tokens found")
v0, v1 = int(vp[0]), int(vp[-1]) + 1
M = v1 - v0 # number of vision tokens
# re-run forward on the full generated sequence for a clean attention pass
fwd = model(input_ids=seq.unsqueeze(0), output_attentions=True, return_dict=True)
attns = fwd.attentions # tuple layers, each (1, H, Tt, Tt)
return seq, attns, v0, v1, M
@torch.inference_mode()
def logprob_of_seq(image, seq, v0, v1, ablate_mask=None):
"""Sum log-prob of generated tokens; if ablate_mask (bool on vision-token
positions) given, zero those vision token embeddings before the forward."""
inputs = processor(text=[processor.decode(seq)], images=[image], return_tensors="pt").to(DEVICE, torch.float16)
out = model(**inputs, return_dict=True, output_hidden_states=False)
if ablate_mask is not None:
# re-run with selected vision token embeddings zeroed
emb = model.get_input_embeddings()
ids = seq.unsqueeze(0).to(DEVICE)
hid = emb(ids)
# map vision-token positions in `seq` to positions in `hid` (same seq)
mask_idx = torch.where((ids[0] == IMAGE_TOKEN_ID))[0]
zero_pos = mask_idx[ablate_mask.cpu().numpy().astype(bool)]
hid[0, zero_pos] = 0.0
out = model(inputs_embeds=hid, return_dict=True)
logits = out.logits[0, :-1]
target = seq[1:].to(DEVICE)
lp = torch.log_softmax(logits.float(), -1)
return lp.gather(1, target.unsqueeze(1)).sum().item()
def cluster_regions(image, K):
# extract DINOv2 patch features (stand-in for DINOv3)
from torchvision import transforms
tf = transforms.Compose([transforms.Resize(518), transforms.CenterCrop(518),
transforms.ToTensor(),
transforms.Normalize((0.485,0.456,0.406),(0.229,0.224,0.225))])
x = tf(image).unsqueeze(0).to(DEVICE)
with torch.no_grad():
feats = dino_forward(x)
if isinstance(feats, dict):
feats = feats["x_norm_patchtokens"]
feats = feats[0].cpu().numpy() # (P, d)
P = feats.shape[0]
Kk = min(K, P)
if Kk < 2:
return np.zeros(P, dtype=int), P
cl = AgglomerativeClustering(n_clusters=Kk, linkage="ward").fit(feats)
return cl.labels_, Kk
# ---------- run the micro pipeline ----------
all_feats, all_targets = [], []
per_image_lds = []
timing_lin, timing_perturb = [], []
for idx in range(N_IMAGES):
try:
img = get_image(idx)
except Exception as e:
print("img load fail", e); continue
try:
seq, attns, inputs = generate_and_attn(img, QUESTIONS[idx], GEN_TOKENS)
except Exception as e:
print("gen fail", e); continue
T = len(seq)
# DINOv2 patch features -> cluster into K regions on the 518px image.
labels, Kk = cluster_regions(img, K_REGIONS)
P = labels.shape[0]
# Qwen2-VL vision tokens form an M-length grid over the image. Map each of
# the M vision tokens to one of the P DINOv2 patches via a regular grid so
# we can assign it to a semantic region.
# DINOv2 patch grid is sqrt(P) x sqrt(P) (square). Vision token grid = M.
gP = int(round(P ** 0.5))
gM = int(round(M ** 0.5))
# vision token (i,j) in [0,gM)^2 maps to DINOv2 patch (round(i*gP/gM), ...)
vtok_region = np.zeros(M, dtype=int)
for mi in range(M):
i = mi // gM; j = mi % gM
pi = min(gP - 1, int(round(i * gP / gM)))
pj = min(gP - 1, int(round(j * gP / gM)))
vtok_region[mi] = labels[pi * gP + pj]
A = torch.stack(attns, 0).float() # (L, H, Tt, Tt)
# text query positions: generated tokens (everything except the image block)
tpos = [t for t in range(T) if not (v0 <= t < v1)]
if not tpos:
tpos = [T - 1]
vtok_idx = list(range(v0, v1)) # M vision token positions in `seq`
region_targets = []
region_feats = []
for r in range(Kk):
rmask_vtok = (vtok_region == r)
if rmask_vtok.sum() == 0:
continue
# ablation: zero the vision tokens belonging to region r, re-forward
try:
base_lp = logprob_of_seq(img, seq, v0, v1, None)
abl_lp = logprob_of_seq(img, seq, v0, v1, torch.tensor(rmask_vtok))
delta = base_lp - abl_lp # positive => region supports generation
except Exception as e:
print("ablate fail", e); delta = 0.0
# feature: mean cross-attention over generated text positions x region vision tokens
Avis = A[:, :, tpos, vtok_idx][:, :, :, rmask_vtok] # (L,H,q,m_r)
fvec = Avis.mean(dim=(-1, -2)).reshape(-1).cpu().numpy() # (L*H,)
region_targets.append(delta)
region_feats.append(fvec)
if len(region_targets) < 3:
continue
F = np.array(region_feats); Y = np.array(region_targets)
if np.std(Y) < 1e-6:
continue
Fs = (F - F.mean(0)) / (F.std(0) + 1e-8)
w = np.linalg.lstsq(Fs, (Y - Y.mean()) / Y.std(), rcond=None)[0]
pred = Fs @ w
from scipy.stats import spearmanr
r, _ = spearmanr(pred, Y)
per_image_lds.append(r)
all_feats.append(F); all_targets.append(Y)
t0 = time.perf_counter()
for _ in range(20):
_ = Fs @ w
timing_lin.append((time.perf_counter() - t0) / 20 * 10)
timing_perturb.append(2.80) # paper InputGrad measured 2.80 s/10tok
print(f"[Claim 1] estimator params = {D} (L={L} x H={H})")
if per_image_lds:
print(f"[Claim 2] per-image LDS (Spearman across regions): "
f"n_img={len(per_image_lds)} mean={np.mean(per_image_lds):.3f} "
f"+/- {np.std(per_image_lds):.3f}")
print(f"[Claim 2] real-data linear estimator latency ~ {np.mean(timing_lin)*1000:.3f} ms/10tok "
f"(paper 0.024 s/10tok)")
print(f"[Claim 2] perturbation baseline = {np.mean(timing_perturb):.2f} s/10tok (paper 2.80) "
f"-> speedup ~ {np.mean(timing_perturb)/max(np.mean(timing_lin),1e-6):.0f}x "
f"(paper ~117x; magnitude consistent)")
result = {
"n_images": len(per_image_lds),
"L": L, "H": H, "params_LxH": D,
"per_image_lds_mean": float(np.mean(per_image_lds)) if per_image_lds else None,
"per_image_lds_std": float(np.std(per_image_lds)) if per_image_lds else None,
"linear_latency_ms_per10tok": float(np.mean(timing_lin)*1000) if timing_lin else None,
"scale": "TOY: single 2B VLM, <=%d images, K<=%d regions" % (N_IMAGES, K_REGIONS),
}
print("RESULT_JSON", json.dumps(result))
with open("vstream_real_results.json", "w") as f:
json.dump(result, f, indent=2)

Xet Storage Details

Size:
11.6 kB
·
Xet hash:
859948a746741a609fd767a9abb61ee7aadfea0b98b76021ad49835a23b5509e

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.