File size: 24,754 Bytes
4a68ea8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
"""
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()