TokenTrace / experiments /tiny_nla /gen_opencode_only.py
cccmmd
feat: add Tiny-NLA activation explanation with trained model weights
9d0d4e9
Raw
History Blame Contribute Delete
3.55 kB
#!/usr/bin/env python3
"""opencode-only teacher label generation - runs in background."""
import json, subprocess, time, sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
ARTIFACTS = Path(__file__).resolve().parents[2] / "artifacts" / "tiny_nla"
RECORDS_FILE = ARTIFACTS / "records_v2.jsonl"
OUTPUT_JSONL = ARTIFACTS / "teacher_labels_v2.jsonl"
PROMPT = """\
你是语言模型内部机制分析专家。用1-2句简洁中文描述Qwen3-0.6B在处理下面这个token时,该位置residual stream激活值编码的语义信息。
上下文:{context}
当前token:「{token}」(位置{pos}/{seq_len})
模型预测下一个词的候选:{top_tokens}
要求:简洁具体,联系token在句中的实际句法/语义角色,联系预测候选推断编码内容,不超过55字。只输出解释本身。"""
def make_prompt(r):
return PROMPT.format(context=r["text"], token=r["token_text"],
pos=r["pos"], seq_len=r["seq_len"],
top_tokens="、".join(r["top_tokens"][:5]))
def strip_think(t):
return t.split("</think>")[-1].strip() if "</think>" in t else t.strip()
def call_opencode(r):
res = subprocess.run(
["opencode", "run", "--model", "opencode/deepseek-v4-flash-free", make_prompt(r)],
capture_output=True, text=True, timeout=240,
)
lines = [l.strip() for l in res.stdout.splitlines()
if l.strip() and not l.startswith("\x1b") and "orchestrator" not in l
and not l.startswith("{") and not l.startswith('"')]
return strip_think("\n".join(lines))
def load_done():
done = set()
try:
for r in json.load(open(ARTIFACTS / "teacher_labels_v2.json")):
done.add((r["text_idx"], r["pos"]))
except Exception:
pass
if OUTPUT_JSONL.exists():
for line in open(OUTPUT_JSONL):
try:
r = json.loads(line)
done.add((r["text_idx"], r["pos"]))
except Exception:
pass
return done
def process(idx_rec):
idx, rec = idx_rec
try:
expl = call_opencode(rec)
if expl and len(expl) >= 5:
return {**rec, "teacher_explanation": expl, "teacher_source": "opencode"}
except Exception as e:
print(f" err [{idx}]: {e}", flush=True)
return None
def main():
workers = int(sys.argv[1]) if len(sys.argv) > 1 else 3
records = [json.loads(l) for l in open(RECORDS_FILE)]
done = load_done()
todo = [(i, r) for i, r in enumerate(records)
if (r["text_idx"], r["pos"]) not in done]
print(f"Remaining: {len(todo)}/9810, workers: {workers}", flush=True)
completed = 0
t0 = time.time()
out_f = open(OUTPUT_JSONL, "a", encoding="utf-8", buffering=1)
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(process, item): item for item in todo}
for fut in as_completed(futures):
result = fut.result()
if result:
out_f.write(json.dumps(result, ensure_ascii=False) + "\n")
completed += 1
if completed % 20 == 0:
rate = completed / (time.time() - t0) * 60
eta_h = (len(todo) - completed) / (rate / 60) / 3600
print(f" [{len(done)+completed}/{9810}] {rate:.0f}/min ETA={eta_h:.1f}h", flush=True)
out_f.close()
print(f"Done: {completed} new labels written", flush=True)
if __name__ == "__main__":
main()