Label-Free Bias-Only Steering
Collection
Bias-only steering vectors from label-free test-time RL (GRPO, majority-vote reward). One repo per benchmark/base model. • 8 items • Updated
A bias-only steering vector for Qwen/Qwen2.5-VL-7B-Instruct, trained with label-free
test-time reinforcement learning: GRPO against a majority-vote
pseudo-label reward, with no ground-truth labels. Only a per-layer additive
bias on each MLP down_proj is trained (~100K bf16 parameters total); the
backbone is frozen throughout.
logicvista.pt — a plain torch.save'd state-dict, one training seed
(the paper reports mean ± std over 3 seeds, so this number varies
slightly from the headline figures)This is not a standalone model — it's a small delta applied on top of
the frozen Qwen/Qwen2.5-VL-7B-Instruct backbone at inference time. Use load_bias.py (below,
also included in this repo as a standalone file) to patch the backbone and
load the bias.
pip install torch transformers pillow # + soundfile/torchaudio for audio tasks
python load_bias.py --image path/to/image.png --question "Is line A steeper than line B?"
#!/usr/bin/env python3
"""Load and apply a label-free bias-only steering vector to Qwen2.5-VL-7B-Instruct.
The checkpoint is a plain state-dict mapping layer names (e.g.
`model.language_model.layers.N.mlp.down_proj.bias`) to a bf16 tensor -- a
per-layer additive bias on the MLP down_proj, trained with test-time GRPO
against a majority-vote pseudo-label reward (no ground-truth labels).
Usage:
python load_bias.py --image path/to/image.png --question "Is line A steeper than line B?"
Full training/eval code: https://github.com/naveenVakada/label-free-bias-steering
"""
import argparse
import importlib
import torch
import torch.nn as nn
def patch_hf_for_bias_vl():
"""Frozen Qwen2.5-VL ships with down_proj.bias=None; add a real bias param
before the model is constructed so the checkpoint's keys have somewhere to go."""
for mod_path, cls_name in [
("transformers.models.qwen2_5_vl.modeling_qwen2_5_vl", "Qwen2_5_VLMLP"),
("transformers.models.qwen2_5_vl.modeling_qwen2_5_vl", "Qwen2MLP"),
]:
try:
mod = importlib.import_module(mod_path)
cls = getattr(mod, cls_name)
except (ImportError, AttributeError):
continue
orig_init = cls.__init__
has_bias_kwarg = cls_name == "Qwen2_5_VLMLP"
def make_patched(orig, has_bias_kwarg):
def patched_with_kwarg(self, config, bias=False):
orig(self, config, bias=bias)
if self.down_proj.bias is None:
old = self.down_proj
self.down_proj = nn.Linear(old.in_features, old.out_features, bias=True)
def patched_no_kwarg(self, config):
orig(self, config)
if self.down_proj.bias is None:
old = self.down_proj
self.down_proj = nn.Linear(old.in_features, old.out_features, bias=True)
return patched_with_kwarg if has_bias_kwarg else patched_no_kwarg
cls.__init__ = make_patched(orig_init, has_bias_kwarg)
def load_steering_bias(model, bias_path):
"""Copy the checkpoint's down_proj.bias tensors into the model in-place."""
bias_dict = torch.load(bias_path, map_location="cpu")
missing, unexpected = model.load_state_dict(bias_dict, strict=False)
loaded = set(bias_dict) - set(unexpected)
print(f" loaded {len(loaded)}/{len(bias_dict)} bias tensors")
return model
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--model_id", default="Qwen/Qwen2.5-VL-7B-Instruct")
ap.add_argument("--bias_path", default="steering_biases.pt")
ap.add_argument("--image", required=True)
ap.add_argument("--question", default="Describe this image.")
ap.add_argument("--max_new_tokens", type=int, default=512)
args = ap.parse_args()
patch_hf_for_bias_vl() # must run before from_pretrained
from PIL import Image
from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration
processor = AutoProcessor.from_pretrained(args.model_id)
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
args.model_id, torch_dtype=torch.bfloat16
)
load_steering_bias(model, args.bias_path)
model.eval()
image = Image.open(args.image)
messages = [{"role": "user", "content": [
{"type": "image", "image": image},
{"type": "text", "text": args.question},
]}]
prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor(text=[prompt], images=[image], return_tensors="pt")
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=args.max_new_tokens, do_sample=False)
response = processor.batch_decode(
out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True
)[0]
print(response)
MIT
Base model
Qwen/Qwen2.5-VL-7B-Instruct