Spaces:
Running
Running
| #!/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() | |