Spaces:
Running
Running
File size: 7,785 Bytes
9d0d4e9 | 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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | #!/usr/bin/env python3
"""
Generate high-quality teacher labels via opencode CLI (or any LLM).
Usage:
python generate_teacher_labels_opencode.py
This script:
1. Reads dataset.jsonl (activation records with context/token/top_tokens)
2. For each record, prints a prompt for the LLM (opencode/Claude/etc.)
3. Reads LLM output and saves to teacher_labels_hq.json
The output file can then be merged into av_training_data.json for retraining.
Run mode options:
--dry-run Print prompts only (for review), no LLM calls
--local Use local Qwen3-0.6B instruct as fallback teacher
--opencode-cmd Path/name of opencode CLI (default: "opencode")
"""
import json, sys, subprocess, argparse, time
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
ARTIFACTS_DIR = REPO_ROOT / "artifacts" / "tiny_nla"
DATASET = ARTIFACTS_DIR / "dataset.jsonl"
OUTPUT = ARTIFACTS_DIR / "teacher_labels_hq.json"
TEACHER_PROMPT = """\
你是一个语言模型内部机制分析专家。我需要你用 1-2 句简洁中文,描述一个 Transformer 语言模型(Qwen3-0.6B)在处理特定 token 时,该位置的 residual stream activation 所编码的语义信息。
---
上下文句子:{context}
当前分析的 token:「{token}」(位置 {pos},共 {seq_len} 个 token)
模型在此位置预测的下一个词(概率最高的候选):{top_tokens}
---
请根据上面信息,用 1-2 句中文描述:
- 模型在这个 token 的位置正在编码什么语义信息
- 这个位置的激活值如何帮助预测后续内容
要求:
- 简洁具体,不要泛泛而谈
- 联系「{token}」在句子中的实际作用
- 联系模型预测的下一个词来推断编码内容
- 不要以"这个位置"或"激活值"开头,而是直接描述语义
- 不超过 60 字
只输出解释本身,不要输出分析过程。"""
def make_prompt(rec):
top5 = "、".join(rec["top_tokens"][:5])
seq_len = len(rec["text"]) # approximate
return TEACHER_PROMPT.format(
context=rec["text"],
token=rec["token_text"],
pos=rec["pos"],
seq_len=seq_len,
top_tokens=top5,
)
def call_opencode(prompt_text, opencode_cmd="opencode", model="opencode/deepseek-v4-flash-free"):
"""Call opencode CLI with a prompt, return response text."""
result = subprocess.run(
[opencode_cmd, "run", "--model", model, prompt_text],
capture_output=True,
text=True,
timeout=120,
)
if result.returncode != 0:
raise RuntimeError(f"opencode error: {result.stderr[:200]}")
# Strip ANSI escape codes and header lines
output = result.stdout
lines = output.splitlines()
# Skip lines starting with ANSI/control chars or "> orchestrator"
content_lines = [
l for l in lines
if l.strip() and not l.strip().startswith("\x1b") and not l.strip().startswith("> orchestrator")
]
return "\n".join(content_lines).strip()
def call_local_qwen(prompt_text):
"""Fallback: use local Qwen3-0.6B instruct."""
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
if not hasattr(call_local_qwen, "_model"):
print(" Loading local Qwen3-0.6B instruct...")
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B", trust_remote_code=True)
mdl = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-0.6B", trust_remote_code=True, dtype=torch.float32,
low_cpu_mem_usage=True, attn_implementation="eager",
)
mdl.eval()
call_local_qwen._tok = tok
call_local_qwen._model = mdl
tok, mdl = call_local_qwen._tok, call_local_qwen._model
msgs = [{"role": "user", "content": prompt_text}]
text = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
inp = tok(text, return_tensors="pt")
with torch.no_grad():
out = mdl.generate(
**inp, max_new_tokens=128, do_sample=False,
pad_token_id=tok.eos_token_id, eos_token_id=tok.eos_token_id,
)
gen = out[0][inp["input_ids"].shape[1]:]
return tok.decode(gen, skip_special_tokens=True).strip()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true", help="Print prompts, no LLM calls")
parser.add_argument("--local", action="store_true", help="Use local Qwen instruct")
parser.add_argument("--opencode-cmd", default="opencode", help="opencode CLI command")
parser.add_argument("--model", default="opencode/deepseek-v4-flash-free", help="opencode model")
parser.add_argument("--limit", type=int, default=None, help="Process only first N records")
parser.add_argument("--skip-existing", action="store_true", default=True, help="Skip if output exists")
args = parser.parse_args()
with open(DATASET) as f:
records = [json.loads(l) for l in f]
if args.limit:
records = records[:args.limit]
# Filter out OOD first-token records (norm > 2000) — they degrade training
records = [r for r in records if r["activation_norm"] < 2000]
print(f"Filtered to {len(records)} in-distribution records (norm < 2000)")
# Load existing results if any
existing = {}
if OUTPUT.exists() and args.skip_existing:
with open(OUTPUT) as f:
existing_list = json.load(f)
existing = {(r["text_idx"], r["pos"]): r for r in existing_list}
print(f"Loaded {len(existing)} existing labels")
results = list(existing.values())
new_count = 0
for i, rec in enumerate(records):
key = (rec["text_idx"], rec["pos"])
if key in existing:
continue
prompt = make_prompt(rec)
if args.dry_run:
print(f"\n=== Record {i}: [{rec['token_text']}] pos={rec['pos']} ===")
print(prompt)
print("---")
continue
try:
if args.local:
explanation = call_local_qwen(prompt)
else:
explanation = call_opencode(prompt, args.opencode_cmd, args.model)
# Clean up: strip think blocks if any
if "<think>" in explanation:
if "</think>" in explanation:
explanation = explanation.split("</think>")[-1].strip()
else:
explanation = explanation.split("<think>")[0].strip()
result = {
"text_idx": rec["text_idx"],
"pos": rec["pos"],
"text": rec["text"],
"token_text": rec["token_text"],
"top_tokens": rec["top_tokens"][:5],
"activation_norm": rec["activation_norm"],
"teacher_explanation": explanation,
"teacher_source": "local_qwen" if args.local else "opencode",
}
results.append(result)
new_count += 1
if new_count % 10 == 0 or i == 0:
# Save checkpoint
with open(OUTPUT, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f" [{i+1}/{len(records)}] saved {len(results)} labels")
print(f" [{rec['token_text']}] → {explanation[:60]}")
time.sleep(0.3) # gentle rate limit
except Exception as e:
print(f" Error on record {i}: {e}", file=sys.stderr)
time.sleep(2)
if not args.dry_run:
with open(OUTPUT, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\nDone. {len(results)} total labels saved to {OUTPUT}")
else:
print(f"\nDry run complete. {len(records)} prompts shown.")
if __name__ == "__main__":
main()
|