Open the think block for non-thinking-default templates, probe answer-slot readability

Qwen3.5 templates close an empty <think></think> by default, so the reader appended a
second <think> and the model saw </think>...<think>. enable_thinking=True fixes the
sequence; Qwen3 is unchanged (pmass 1.000 before and after).

The probe exists because Qwen3.5-0.8B reads the WVS battery at pmass 0.61-0.84, not
0.999, and a mushy readout would waste the big run.

Co-Authored-By: Claude <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassnameandClaude committed 2026-09-18 19:31:58 +08:00
1 parent 3cd62abf2a
commit 9109dbfe19
6 files changed
+125 -9

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
"""Does the WVS answer-slot readout hold on this model family, and at what think budget?
Qwen3-0.6B answers the IW battery with pmass 0.999. Qwen3.5-0.8B read 0.783 at think=1 in the
steering smoke, which would make every steered coordinate mushy. Before renting a big GPU, find out
whether that is the think budget (the model is mid-thought when we force the answer slot) or the
chat template (the prefill does not land where we think it does).
uv run --extra steer python scripts/probe_wvs_think_budget.py --model Qwen/Qwen3.5-0.8B
"""
from __future__ import annotations
import argparse
import numpy as np
import torch
from loguru import logger
from tabulate import tabulate
from transformers import AutoModelForCausalLM, AutoTokenizer
from moralmaps.iw_axes import resolve_items
from moralmaps.read import read_items, resolve_answer_ids
from moralmaps.wvs import build_instruments, load_wvs_all, model_axis_scores, read_model
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="Qwen/Qwen3.5-0.8B")
ap.add_argument("--think-budgets", default="1,16,64,256")
ap.add_argument("--batch-size", type=int, default=12)
ap.add_argument("--device", default="cuda")
ap.add_argument("--device-map", default=None, help="'auto' shards a large model over the GPUs")
args = ap.parse_args()
tok = AutoTokenizer.from_pretrained(args.model)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
tok.padding_side = "left"
if args.device_map:
model = AutoModelForCausalLM.from_pretrained(args.model, dtype=torch.bfloat16,
device_map=args.device_map).eval()
else:
model = AutoModelForCausalLM.from_pretrained(args.model, dtype=torch.bfloat16).to(args.device).eval()
resolved = resolve_items(load_wvs_all())
instrs, meta = build_instruments(resolved)
rows = []
for think in [int(t) for t in args.think_budgets.split(",")]:
read = []
for k, instr in enumerate(instrs):
read += read_items(model, tok, instr, instr.items,
resolve_answer_ids(tok, instr.answer_space),
max_think_tokens=think, batch_size=args.batch_size,
verbose_first=(k == 0 and think == 1))
pm = [r["pmass_allowed"] for r in read]
x, y = model_axis_scores(read_model(read, meta), meta, resolved)
closed = sum(r["emitted_close"] for r in read)
rows.append([think, f"{np.mean(pm):.3f}", f"{np.min(pm):.3f}", f"{closed}/{len(read)}",
f"{x:.4f}", f"{y:.4f}"])
logger.info(f"think={think}: mean pmass {np.mean(pm):.3f}")
print(tabulate(rows, tablefmt="pipe", headers=[
"think tokens", "mean pmass", "min pmass", "closed think", "x", "y"]))
print("\nSHOULD: pmass climbs toward ~1.0 as the think budget grows, and the coordinate settles.\n"
"ELSE, if pmass stays low at every budget, the prefill or chat template is wrong for this\n"
"family and no steered coordinate from it is comparable to the published map.")
if __name__ == "__main__":
main()
+24
View File
@@ -83,6 +83,30 @@ def main(model: str = MODEL, methods: str = ",".join(METHODS), seeds: str = ",".
print(f"{method}\ts{seed}\tFAILED\t{error}")
@app.function(gpu=os.environ.get("WVS_GPU", "H200"), volumes={"/cache": cache}, timeout=60 * 60)
def probe(model: str, device_map: str) -> str:
"""Read the battery unsteered at several think budgets: is this model's answer slot readable?"""
from huggingface_hub import snapshot_download
snapshot_download(model)
argv = ["--model", model] + (["--device-map", device_map] if device_map else [])
out = subprocess.run([sys.executable, "scripts/probe_wvs_think_budget.py", *argv],
cwd="/repo", check=True, capture_output=True, text=True)
return out.stdout
@app.local_entrypoint()
def readable(models: str = "Qwen/Qwen3.5-27B,Qwen/Qwen3-32B", device_map: str = ""):
"""Which candidate large model reads cleanly enough to be worth a sweep? ~10 min per model."""
handles = {m: probe.spawn(m, device_map) for m in models.split(",")}
for m, handle in handles.items():
print(f"\n===== {m} =====")
try:
print(handle.get())
except Exception as error:
print(f"FAILED\t{error}")
@app.local_entrypoint()
def smoke():
"""Same image, mounts and Volume as the real fan-out, on the tiny random model."""
+3 -1
View File
@@ -141,7 +141,9 @@ def main() -> None:
else:
model = AutoModelForCausalLM.from_pretrained(args.model, dtype=dtype).to(args.device).eval()
n_blocks = model.config.num_hidden_layers
# Qwen3.5 ships as a VL wrapper (Qwen3_5ForConditionalGeneration), so the block count lives in
# config.text_config, not at the top level.
n_blocks = model.config.get_text_config().num_hidden_layers
if args.layers == "mid":
layers = tuple(range(max(2, int(n_blocks * 0.2)), min(n_blocks - 2, int(n_blocks * 0.8))))
else: