""" Depth Comparison Benchmark - ThinkingDial 核心验证实验 目标:验证 ThinkingDial 的不同 depth 在数学推理任务上的表现差异。 这是证明 ThinkingDial 机制有效性的关键实验。 实验设计: 1. 扩大模型配置到 ~10M 参数(hidden=512, layers=8, heads=8) 2. 用合成数学数据(四则运算)预训练所有 depth 3. 用 GRPO + GSM8K 奖励函数分别在不同 depth 下微调 4. 输出 depth 准确率对比表 Usage: python experiments/depth_benchmark.py """ import sys import math import random import time import torch import torch.nn.functional as F from pathlib import Path from typing import Dict, List, Tuple sys.path.insert(0, str(Path(__file__).parent.parent)) from models.fusion_model import FusionModel, FusionConfig from models.thinking_dial import ( ThinkingDialModel, ThinkingConfig, GRPOTrainer, GRPOConfig ) from evaluation.gsm8k_reward import GSM8KEvaluator, extract_answer, normalize_answer DEVICE = "cuda" if torch.cuda.is_available() else "cpu" print(f"[Benchmark] Device: {DEVICE}") print() # ─── 实验配置 ─────────────────────────────────────────────────────────────── EXP_CONFIG = { # ~5M 参数模型配置(CPU 可在合理时间内完成) "hidden_size": 256, "num_hidden_layers": 6, "num_attention_heads": 8, "intermediate_size": 512, "vocab_size": 1000, "max_position_embeddings": 128, "block_size": 32, "latent_dim": 16, # 训练参数 "pretrain_epochs": 100, "pretrain_batch_size": 8, "pretrain_lr": 1e-3, "grpo_epochs": 30, "grpo_batch_size": 4, "grpo_lr": 1e-4, "grpo_num_samples": 3, "num_thinking_depths": 4, "test_batch_size": 50, "seed": 42, } def estimate_params(config: FusionConfig) -> int: """粗略估计模型参数量""" emb = config.vocab_size * config.hidden_size per_layer = ( # attention: Q, K, V, O projections 4 * config.hidden_size * config.hidden_size + # FFN: gate, up, down 3 * config.hidden_size * config.intermediate_size + # SBLA latent projections (approx) 2 * config.hidden_size * config.latent_dim + # norms 2 * config.hidden_size ) return emb + config.num_hidden_layers * per_layer + config.hidden_size * config.vocab_size # ─── 合成数学数据 ────────────────────────────────────────────────────────── def generate_math_data(n: int = 2000, ops: List[str] = None, max_val: int = 100, seed: int = 42) -> List[Tuple[str, int, int, int]]: """ 生成合成数学运算数据。 格式: (x, op_code, y, result) op_code: 0=+, 1=-, 2=*, 3=/ """ if ops is None: ops = ["+", "-"] # 先只做加减法(乘除需要更大模型) op_map = {"+": 0, "-": 1, "*": 2, "/": 3} rng = random.Random(seed) data = [] for _ in range(n): op = rng.choice(ops) code = op_map[op] if op == "+": x = rng.randint(1, max_val) y = rng.randint(1, max_val) result = x + y elif op == "-": x = rng.randint(1, max_val * 2) y = rng.randint(1, x) # 保证结果 >= 0 result = x - y elif op == "*": x = rng.randint(2, 20) y = rng.randint(2, 20) result = x * y elif op == "/": y = rng.randint(2, 20) result = rng.randint(1, 20) x = y * result # 保证整除 # 结果 clamp 到 vocab 范围 result = max(0, min(result, EXP_CONFIG["vocab_size"] - 2)) data.append((x, code, y, result)) return data def encode_example(x: int, op: int, y: int, result: int) -> Tuple[List[int], List[int]]: """ 编码数学题: input: [2, x, op, y, 0, 0, 0] (7 tokens, padded) labels: [-100, x, op, y, 99, result, 1] (7 tokens, same length) Loss 计算: predict y->99, 99->result, result->1 """ input_ids = [2, x, op, y, 0, 0, 0] labels = [-100, x, op, y, 99, result, 1] return input_ids, labels # ─── 梯度化奖励函数 ──────────────────────────────────────────────────────── def gradient_reward_fn(prompt: str, response: str) -> float: """ 梯度化奖励函数(用于合成数学任务): - 能提取到数字 → +0.1 - 数值接近正确值(误差 < 10%)→ +0.3 - 完全正确 → +1.0 """ extracted = extract_answer(response) if extracted is None: return 0.0 # 查找 ground truth gold = None try: # 从 prompt 中提取数字: CLS x OP y tokens = prompt.strip().split() if len(tokens) >= 3: x, op_code, y = int(tokens[0]), int(tokens[1]), int(tokens[2]) if op_code == 0: gold = x + y elif op_code == 1: gold = x - y elif op_code == 2: gold = x * y elif op_code == 3: gold = x // y if y != 0 else 0 except (ValueError, IndexError): pass if gold is None: return 0.1 # 至少输出了数字 extracted_norm = normalize_answer(extracted) gold_norm = normalize_answer(gold) if extracted_norm == gold_norm: return 1.0 # 检查是否接近(误差 < 10%) if gold != 0: rel_error = abs(extracted_norm - gold_norm) / abs(gold_norm) if rel_error < 0.1: return 0.3 # 输出了数字但答案错误 return 0.1 # ─── 实验核心 ─────────────────────────────────────────────────────────────── def pretrain_model(config: FusionConfig, data: list) -> ThinkingDialModel: """阶段 1:用合成数学数据预训练""" model = FusionModel(config) thinking_config = ThinkingConfig( num_thinking_depths=EXP_CONFIG["num_thinking_depths"] ) td_model = ThinkingDialModel(model, thinking_config) td_model.train().to(DEVICE) optimizer = torch.optim.AdamW(td_model.parameters(), lr=EXP_CONFIG["pretrain_lr"]) losses = [] for epoch in range(EXP_CONFIG["pretrain_epochs"]): batch = random.sample(data, min(EXP_CONFIG["pretrain_batch_size"], len(data))) input_batch = [] target_batch = [] for x, op, y, result in batch: inp, tgt = encode_example(x, op, y, result) input_batch.append(inp) target_batch.append(tgt) input_ids = torch.tensor(input_batch, device=DEVICE, dtype=torch.long) labels = torch.tensor(target_batch, device=DEVICE, dtype=torch.long) optimizer.zero_grad() outputs = td_model(input_ids, labels=labels) loss = outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(td_model.parameters(), 1.0) optimizer.step() losses.append(loss.item()) if (epoch + 1) % 10 == 0: avg = sum(losses[-10:]) / 10 print(f" Pretrain Epoch {epoch+1}/{EXP_CONFIG['pretrain_epochs']}: " f"Loss = {avg:.4f}") final_loss = sum(losses[-10:]) / 10 print(f" Pretrain complete. Final loss: {final_loss:.4f}") return td_model def grpo_finetune(td_model: ThinkingDialModel, data: list, depth: int, gsm8k_eval: GSM8KEvaluator = None) -> Dict: """ 阶段 2:GRPO 微调(使用合成数学数据 + 可选 GSM8K 奖励) Returns: {losses, rewards, depth} """ grpo_config = GRPOConfig( grpo_sample_size=EXP_CONFIG["grpo_num_samples"], kl_coef=0.05, ) # 设置奖励函数 def reward_fn(prompt, response): if gsm8k_eval is not None: return gsm8k_eval.reward(prompt, response) return gradient_reward_fn(prompt, response) td_model.train() optimizer = torch.optim.AdamW( [p for p in td_model.parameters() if p.requires_grad], lr=EXP_CONFIG["grpo_lr"] ) epoch_losses = [] epoch_rewards = [] for epoch in range(EXP_CONFIG["grpo_epochs"]): batch = random.sample(data, min(EXP_CONFIG["grpo_batch_size"], len(data))) batch_losses = [] batch_rewards = [] for x, op, y, result in batch: inp, _ = encode_example(x, op, y, result) input_ids = torch.tensor([inp], device=DEVICE, dtype=torch.long) # 用指定 depth 生成多个样本 gen_ids = [] for _ in range(EXP_CONFIG["grpo_num_samples"]): with torch.no_grad(): out = td_model.generate( input_ids, max_new_tokens=8, thinking_depth=depth, do_sample=True, temperature=1.0, pad_token_id=0, ) gen_ids.append(out[0].tolist()) # 计算奖励 rewards = [] for gen in gen_ids: new_tokens = gen[len(inp):] # Extract result: expected format [99, result, 1, ...] if len(new_tokens) >= 2 and new_tokens[0] == 99: predicted = new_tokens[1] else: predicted = None # Compute reward if predicted is not None and predicted == result: r = 1.0 elif predicted is not None: # Gradient reward: check proximity if result != 0 and abs(predicted - result) / abs(result) < 0.1: r = 0.3 else: r = 0.1 else: r = 0.0 rewards.append(r) # GRPO: 基于奖励计算 loss if len(rewards) > 1 and sum(rewards) > 0: # 归一化奖励 rewards_t = torch.tensor(rewards, device=DEVICE, dtype=torch.float) mean_r = rewards_t.mean() std_r = rewards_t.std() + 1e-8 advantages = (rewards_t - mean_r) / std_r # 对每个样本计算 policy gradient loss total_loss = torch.tensor(0.0, device=DEVICE) for i, gen in enumerate(gen_ids): gen_t = torch.tensor([gen], device=DEVICE, dtype=torch.long) # Mask labels: only compute loss on generated tokens gen_labels = gen_t[:, 1:].clone() gen_labels[:, :len(inp)-1] = -100 # ignore input portion # Forward to get log probs outputs = td_model(gen_t[:, :-1], labels=gen_labels) neg_log_prob = outputs.loss # Policy gradient: -advantage * log_prob total_loss += advantages[i] * neg_log_prob loss = total_loss / len(gen_ids) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(td_model.parameters(), 1.0) optimizer.step() batch_losses.append(loss.item()) batch_rewards.append(mean_r.item()) else: batch_losses.append(0.0) batch_rewards.append(0.0) epoch_losses.append(sum(batch_losses) / len(batch_losses)) epoch_rewards.append(sum(batch_rewards) / len(batch_rewards)) if (epoch + 1) % 10 == 0: print(f" GRPO Epoch {epoch+1}/{EXP_CONFIG['grpo_epochs']}: " f"Loss = {epoch_losses[-1]:.4f}, " f"Mean Reward = {epoch_rewards[-1]:.4f}") print(f" GRPO complete. Final reward: {epoch_rewards[-1]:.4f}") return { "losses": epoch_losses, "rewards": epoch_rewards, "depth": depth, } def evaluate_accuracy(td_model: ThinkingDialModel, data: list, depth: int, n_samples: int = None) -> Dict: """ 在测试集上评估指定 depth 的准确率。 期望生成格式: [..., 99, result, 1, ...] -> 取 position 5 的 token 作为结果 """ if n_samples is None: n_samples = EXP_CONFIG["test_batch_size"] test_data = random.sample(data, min(n_samples, len(data))) correct = 0 total = len(test_data) results_by_op = {"+": {"correct": 0, "total": 0}, "-": {"correct": 0, "total": 0}, "*": {"correct": 0, "total": 0}, "/": {"correct": 0, "total": 0}} td_model.eval() op_names = {0: "+", 1: "-", 2: "*", 3: "/"} for x, op, y, result in test_data: inp, _ = encode_example(x, op, y, result) input_ids = torch.tensor([inp], device=DEVICE, dtype=torch.long) with torch.no_grad(): out = td_model.generate( input_ids, max_new_tokens=8, thinking_depth=depth, do_sample=False, pad_token_id=0, ) gen_tokens = out[0, len(inp):].tolist() # Expected: model generates [99, result, 1, ...] gen_result = None if len(gen_tokens) >= 2 and gen_tokens[0] == 99: gen_result = gen_tokens[1] elif gen_tokens: gen_result = gen_tokens[0] op_name = op_names.get(op, "?") results_by_op[op_name]["total"] += 1 if gen_result is not None: if normalize_answer(gen_result) == normalize_answer(result): correct += 1 results_by_op[op_name]["correct"] += 1 accuracy = correct / total if total > 0 else 0.0 return { "depth": depth, "accuracy": accuracy, "correct": correct, "total": total, "by_op": results_by_op, } # ─── 消融实验 ─────────────────────────────────────────────────────────────── def create_vanilla_model(config: FusionConfig): """ 创建无 ThinkingDial 的 vanilla Transformer 对照组。 直接用 FusionModel(无 thinking depth 偏置)。 """ model = FusionModel(config) model.train().to(DEVICE) return model def pretrain_vanilla(model: FusionModel, data: list) -> Dict: """Vanilla Transformer 预训练(无 ThinkingDial)""" optimizer = torch.optim.AdamW(model.parameters(), lr=EXP_CONFIG["pretrain_lr"]) losses = [] for epoch in range(EXP_CONFIG["pretrain_epochs"]): batch = random.sample(data, min(EXP_CONFIG["pretrain_batch_size"], len(data))) input_batch = [] target_batch = [] for x, op, y, result in batch: inp, tgt = encode_example(x, op, y, result) input_batch.append(inp) target_batch.append(tgt) input_ids = torch.tensor(input_batch, device=DEVICE, dtype=torch.long) labels = torch.tensor(target_batch, device=DEVICE, dtype=torch.long) optimizer.zero_grad() outputs = model(input_ids, labels=labels) loss = outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() losses.append(loss.item()) final_loss = sum(losses[-10:]) / 10 return {"losses": losses, "final_loss": final_loss} def evaluate_vanilla(model: FusionModel, data: list, n_samples: int = None) -> Dict: """评估 vanilla Transformer 准确率""" if n_samples is None: n_samples = EXP_CONFIG["test_batch_size"] test_data = random.sample(data, min(n_samples, len(data))) correct = 0 total = len(test_data) model.eval() for x, op, y, result in test_data: inp, _ = encode_example(x, op, y, result) input_ids = torch.tensor([inp], device=DEVICE, dtype=torch.long) with torch.no_grad(): out = model.generate( input_ids, max_new_tokens=8, do_sample=False, pad_token_id=0, ) gen_tokens = out[0, len(inp):].tolist() gen_result = None if len(gen_tokens) >= 2 and gen_tokens[0] == 99: gen_result = gen_tokens[1] else: gen_result = extract_answer(" ".join(str(t) for t in gen_tokens)) if gen_result is not None: if normalize_answer(gen_result) == normalize_answer(result): correct += 1 accuracy = correct / total if total > 0 else 0.0 return {"accuracy": accuracy, "correct": correct, "total": total} # ─── 主实验流程 ────────────────────────────────────────────────────────────── def run_benchmark(): print("=" * 70) print("ThinkingDial Depth Comparison Benchmark") print("=" * 70) print() random.seed(EXP_CONFIG["seed"]) torch.manual_seed(EXP_CONFIG["seed"]) # 配置模型 config = FusionConfig( vocab_size=EXP_CONFIG["vocab_size"], hidden_size=EXP_CONFIG["hidden_size"], num_hidden_layers=EXP_CONFIG["num_hidden_layers"], num_attention_heads=EXP_CONFIG["num_attention_heads"], intermediate_size=EXP_CONFIG["intermediate_size"], max_position_embeddings=EXP_CONFIG["max_position_embeddings"], block_size=EXP_CONFIG["block_size"], latent_dim=EXP_CONFIG["latent_dim"], ) n_params = estimate_params(config) print(f"Model config: {EXP_CONFIG['hidden_size']}d x {EXP_CONFIG['num_hidden_layers']}L x {EXP_CONFIG['num_attention_heads']}H") print(f"Estimated parameters: ~{n_params:,}") print() # 生成训练数据 train_data = generate_math_data(n=500, ops=["+"], max_val=20, seed=42) test_data = generate_math_data(n=100, ops=["+"], max_val=20, seed=999) print(f"Training set: {len(train_data)} examples") print(f"Test set: {len(test_data)} examples") print() # ─── 实验 1: Depth 对比(相同模型,不同 depth) ───────────────────── print("=" * 60) print("EXPERIMENT 1: Depth Comparison (Same Model, Different Depths)") print("=" * 60) print() t0 = time.time() print("[1/4] Pretraining ThinkingDial model...") td_model = pretrain_model(config, train_data) print(f" Time: {time.time() - t0:.1f}s") print() # 在预训练后立即评估所有 depth(零样本) print("[2/4] Zero-shot accuracy by depth (before GRPO)...") zero_shot_results = {} for depth in range(EXP_CONFIG["num_thinking_depths"]): result = evaluate_accuracy(td_model, test_data, depth) zero_shot_results[depth] = result print(f" Depth {depth}: {result['accuracy']*100:.1f}% " f"({result['correct']}/{result['total']})") print() # 对每个 depth 进行 GRPO 微调(共享预训练权重) print("[3/4] GRPO fine-tuning per depth...") grpo_results = {} for depth in range(EXP_CONFIG["num_thinking_depths"]): print(f" --- Depth {depth} ---") # 每次从预训练检查点开始 td_model_copy = pretrain_model(config, train_data) # 重新预训练保证公平 grpo_res = grpo_finetune(td_model_copy, train_data, depth) grpo_results[depth] = grpo_res # 评估 result = evaluate_accuracy(td_model_copy, test_data, depth) grpo_results[depth]["eval"] = result print(f" Post-GRPO Accuracy: {result['accuracy']*100:.1f}%") print() # 清理 del td_model_copy if DEVICE == "cuda": torch.cuda.empty_cache() print("[4/4] Post-GRPO accuracy by depth...") post_grpo_results = {} for depth in range(EXP_CONFIG["num_thinking_depths"]): result = grpo_results[depth]["eval"] post_grpo_results[depth] = result print(f" Depth {depth}: {result['accuracy']*100:.1f}% " f"({result['correct']}/{result['total']})") print() # ─── 实验 2: 消融实验(Vanilla vs ThinkingDial) ────────────────── print("=" * 60) print("EXPERIMENT 2: Ablation (Vanilla Transformer vs ThinkingDial)") print("=" * 60) print() print("[1/2] Pretraining vanilla Transformer...") t1 = time.time() vanilla_model = create_vanilla_model(config) vanilla_train = pretrain_vanilla(vanilla_model, train_data) print(f" Pretrain loss: {vanilla_train['final_loss']:.4f}") print(f" Time: {time.time() - t1:.1f}s") print() print("[2/2] Evaluating vanilla Transformer...") vanilla_result = evaluate_vanilla(vanilla_model, test_data) print(f" Vanilla accuracy: {vanilla_result['accuracy']*100:.1f}% " f"({vanilla_result['correct']}/{vanilla_result['total']})") print() # ─── 结果汇总 ────────────────────────────────────────────────────── print() print("=" * 70) print("RESULTS SUMMARY") print("=" * 70) print() print("Table 1: Zero-shot Accuracy by Thinking Depth") print("-" * 50) print(f"{'Depth':<10} {'Accuracy':<12} {'Correct/Total':<15}") print("-" * 50) for depth in range(EXP_CONFIG["num_thinking_depths"]): r = zero_shot_results[depth] print(f"{depth:<10} {r['accuracy']*100:>6.1f}% {r['correct']}/{r['total']}") print() print("Table 2: Post-GRPO Accuracy by Thinking Depth") print("-" * 50) print(f"{'Depth':<10} {'Accuracy':<12} {'Correct/Total':<15} {'Final Reward':<15}") print("-" * 50) for depth in range(EXP_CONFIG["num_thinking_depths"]): r = grpo_results[depth]["eval"] reward = grpo_results[depth]["rewards"][-1] print(f"{depth:<10} {r['accuracy']*100:>6.1f}% " f"{r['correct']}/{r['total']:<10} {reward:>8.4f}") print() print("Table 3: Ablation Comparison") print("-" * 50) print(f"{'Model':<25} {'Accuracy':<12} {'Correct/Total':<15}") print("-" * 50) # ThinkingDial best depth best_depth = max(range(EXP_CONFIG["num_thinking_depths"]), key=lambda d: post_grpo_results[d]["accuracy"]) td_acc = post_grpo_results[best_depth] print(f"ThinkingDial (depth={best_depth}) {td_acc['accuracy']*100:>6.1f}% " f"{td_acc['correct']}/{td_acc['total']}") print(f"Vanilla Transformer {vanilla_result['accuracy']*100:>6.1f}% " f"{vanilla_result['correct']}/{vanilla_result['total']}") print() # 分析 td_best_acc = post_grpo_results[best_depth]["accuracy"] vanilla_acc = vanilla_result["accuracy"] print("Analysis:") if td_best_acc > vanilla_acc: improvement = (td_best_acc - vanilla_acc) / vanilla_acc * 100 print(f" ThinkingDial (depth={best_depth}) outperforms vanilla by +{improvement:.1f}%") elif td_best_acc == vanilla_acc: print(f" ThinkingDial matches vanilla performance") else: degradation = (vanilla_acc - td_best_acc) / vanilla_acc * 100 print(f" ThinkingDial underperforms vanilla by -{degradation:.1f}%") print(f" Note: This may indicate architecture needs adjustment or training is insufficient") # Depth variance analysis accuracies = [post_grpo_results[d]["accuracy"] for d in range(EXP_CONFIG["num_thinking_depths"])] if max(accuracies) > 0 and min(accuracies) < max(accuracies): print(f" Depth sensitivity: accuracy ranges from " f"{min(accuracies)*100:.1f}% (depth {accuracies.index(min(accuracies))}) " f"to {max(accuracies)*100:.1f}% (depth {accuracies.index(max(accuracies))})") print(f" ThinkingDial produces different outcomes at different depths") print() total_time = time.time() - t0 print(f"Total benchmark time: {total_time:.1f}s") print() return { "zero_shot": zero_shot_results, "post_grpo": post_grpo_results, "grpo": grpo_results, "vanilla": vanilla_result, } if __name__ == "__main__": results = run_benchmark()