File size: 6,212 Bytes
4764138 | 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 | """Hidden-state feature of the hidden-state arms (EXPERIMENT_LOG.md 2026-10-19, tasks 3 and 4a) and its HTTP server.
Runs in .train-venv.
Feature: Qwen3.5-9B's last-layer hidden state (after the final norm, i.e. the lm_head input; 4096 dims) at the last
prompt token of A's grounding question (ctrl_policy.ground(): the frame, GROUND.format(name), thinking off, the answer
started as START, rendered by the chat template with continue_final_message as vLLM renders it), prefill only. Batches
are right-padded and each row's last real token is gathered. Values travel as base64 of the bf16 bits (encode/decode;
hid_policy.decode is the numpy twin), so a logged feature is exactly the one the controller used.
Server: one model replica per GPU (threads), a shared queue, each replica takes up to MAX_BATCH waiting requests per
forward. POST /feat {"frame": base64 PNG, "name": target name} -> {"feat": ...} and, with --base, also "base": the
feature with the adapter disabled (the 4a KL reference input). The port opens once every replica is loaded.
Usage: CUDA_VISIBLE_DEVICES=2 .train-venv/bin/python train/hid_server.py --port 9210 [--gpus 0] [--adapter DIR --base]
"""
import argparse
import base64
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import io
import json
from pathlib import Path
import queue
import sys
import threading
import time
import torch
from PIL import Image
from transformers import AutoProcessor
from bt_train import MODULES, load_model # noqa: F401 (MODULES: the LoRA targets, re-exported for hid_grpo)
from ctrl_policy import START
ROOT = Path(__file__).resolve().parents[1]
sys.path.append(str(ROOT / "bench"))
from diag_perception_direction import GROUND # noqa: E402
MAX_BATCH = 32
def processor():
return AutoProcessor.from_pretrained(ROOT / "models/Qwen3.5-9B", local_files_only=True)
def prompt(proc, name):
return proc.apply_chat_template(
[{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": GROUND.format(name)}]},
{"role": "assistant", "content": START}],
tokenize=False, add_generation_prompt=False, continue_final_message=True, enable_thinking=False)
def batch_inputs(proc, images, names):
return dict(proc(text=[prompt(proc, n) for n in names], images=images, padding=True, padding_side="right",
return_tensors="pt"))
def features(model, inputs, device):
"""(B, 4096) bf16 last-layer hidden states at each row's last prompt token; model: the Qwen3.5 model or its
PeftModel (grad follows the caller's mode)."""
body = (model.get_base_model() if hasattr(model, "get_base_model") else model).model
x = {k: v.to(device) for k, v in inputs.items()}
with torch.autocast("cuda", dtype=torch.bfloat16):
h = body(**x, use_cache=False).last_hidden_state
return h[torch.arange(len(h), device=device), x["attention_mask"].sum(1) - 1]
def encode(f):
"""bf16 vector -> base64 of its bits."""
return base64.b64encode(f.to(torch.bfloat16).view(torch.int16).cpu().numpy().tobytes()).decode()
def decode(s):
return torch.frombuffer(bytearray(base64.b64decode(s)), dtype=torch.int16).view(torch.bfloat16)
def png(b64):
return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, required=True)
ap.add_argument("--gpus", default="0", help="local cuda indices, one replica each")
ap.add_argument("--adapter", type=Path)
ap.add_argument("--base", action="store_true", help="also return the feature with the adapter disabled")
args = ap.parse_args()
proc = processor()
jobs = queue.Queue()
def replica(model, device):
while True:
items = [jobs.get()]
while len(items) < MAX_BATCH:
try:
items.append(jobs.get_nowait())
except queue.Empty:
break
try:
inputs = batch_inputs(proc, [png(i["frame"]) for i in items], [i["name"] for i in items])
with torch.no_grad():
out = {"feat": features(model, inputs, device)}
if args.base:
with model.disable_adapter():
out["base"] = features(model, inputs, device)
for k, item in enumerate(items):
item["out"] = {key: encode(f[k]) for key, f in out.items()}
except Exception as e: # answer every waiting request rather than leave it hanging
for item in items:
item["out"] = {"error": f"{type(e).__name__}: {e}"}
for item in items:
item["done"].set()
gpus = [int(g) for g in args.gpus.split(",")]
for g in gpus:
model = load_model(g)
if args.adapter:
from peft import PeftModel
model = PeftModel.from_pretrained(model, args.adapter)
model.eval()
threading.Thread(target=replica, args=(model, torch.device("cuda", g)), daemon=True).start()
class Handler(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def reply(self, code, body):
data = json.dumps(body).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
self.reply(200, {"gpus": gpus})
def do_POST(self):
item = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
item["done"] = threading.Event()
jobs.put(item)
item["done"].wait()
self.reply(500 if "error" in item["out"] else 200, item["out"])
server = ThreadingHTTPServer(("0.0.0.0", args.port), Handler)
server.daemon_threads = True
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} feature server on port {args.port}, gpus {gpus}, adapter "
f"{args.adapter}, base {args.base}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()
|