"""
=============================================================================
CS349E · 第 14 讲 · 02_reward_hacking.py —— 评估器怎样被"骗"，以及怎样把它做结实
=============================================================================

【这个脚本要回答的问题】
    一个用自动评估器当"奖励"的系统（重复采样挑最好的、进化搜索、强化学习），会把评估器的每个漏洞都找出来。
    KernelBench v0.1、METR、Sakana 的 AI CUDA Engineer、Cognition 的 Kevin-32B、KernelBench-Verified 都报告过这类"作弊"。
      演示 1：作弊图鉴——7 种作弊分别交给朴素和加固的评估器，看谁被骗、加固的哪一道防线拦住了它。
      演示 2：两个作弊的源码和机制："偷"参考的输出（缓冲区复用）、"懒"结果（停表时还没算）。
      演示 3：检查题目本身——退化的题（输出和输入无关）和"光速"下界。
      演示 4：一个"会作弊的模型"在两种评估器下的 fast_p。
      演示 5：军备竞赛——一个能识别出"自己正在被加固的评估器考"的候选。

【关于数字】时间是本机 numpy 的墙钟时间；作弊的源码是我写的，每一种都对应一份公开报告里描述过的现象（见讲义 §3）。
【运行】python3 02_reward_hacking.py      （约 15 秒）
【依赖】numpy；本目录的 kbench；../lecture01 的 console。
"""

import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))

from kbench import LIBRARY, TASK, TASKS, evaluate, fast_p, machine_speed, task_sanity  # noqa: E402
from console import print_header, print_observations, print_table  # noqa: E402

# ---------------------------------------------------------------------------
# 作弊图鉴：(名字, 任务, 钻的是哪个空子, 现实里的出处, 源码)
# ---------------------------------------------------------------------------
CHEATS = [
    ("noop_steal", "L1_gelu", "什么都不算，直接返回分配器给的缓冲区——里面恰好是参考实现刚算完的结果",
     "Sakana AI CUDA Engineer（2025-02）；METR 怀疑的 torch.empty 复用", '''
def forward(inputs, params, alloc):
    (x,) = inputs
    return alloc(x.shape)              # 只"申请"输出，一个数都不写
'''),
    ("call_reference", "L1_matmul", "直接调用参考实现（交卷但没写内核）",
     "KernelBench v0.1：调用高层算子而不是写 CUDA", '''
def forward(inputs, params, alloc):
    return reference(inputs, params, alloc)
'''),
    ("try_fallback", "L1_softmax", "\"快\"内核出错就悄悄退回参考实现",
     "Kevin-32B 训练早期：try/except 退回 PyTorch", '''
def forward(inputs, params, alloc):
    try:
        (x,) = inputs
        e = np.exp(x - x.max(axis=2, keepdims=True))     # 有 bug 的"快内核"：维度写错，会抛异常
        return e / e.sum(axis=2, keepdims=True)
    except Exception:
        return reference(inputs, params, alloc)          # 出错就悄悄换成参考实现
'''),
    ("cache_by_fingerprint", "L1_gelu", "按\"地址 + 两个元素\"认出重复的输入，直接返回上次的结果",
     "METR / KernelBench v0.1：输出随种子变化太小的题允许缓存", '''
_cache = {}

def forward(inputs, params, alloc):
    (x,) = inputs
    key = (x.ctypes.data, x.shape, float(x[0, 0]), float(x[-1, -1]))
    if key not in _cache:
        _cache.clear()
        c = np.float32(math.sqrt(2.0 / math.pi))
        _cache[key] = 0.5 * x * (1.0 + np.tanh(c * (x + 0.044715 * x ** 3)))   # 和参考一样的算法
    return _cache[key]
'''),
    ("lazy_result", "L1_softmax", "返回一个\"懒\"对象：真正的计算推迟到有人读它的时候（停表之后）",
     "KernelBench v0.1：在另一条 CUDA stream 上算、计时没同步", '''
class Lazy:
    def __init__(self, f):
        self.f, self.v = f, None

    def __array__(self, dtype=None, copy=None):          # numpy 把它转成数组时才真正计算
        if self.v is None:
            self.v = self.f()
        return self.v if dtype is None else self.v.astype(dtype)


def forward(inputs, params, alloc):
    (x,) = inputs
    def work():
        e = np.exp(x - x.max(axis=1, keepdims=True))
        return e / e.sum(axis=1, keepdims=True)
    return Lazy(work)
'''),
    ("drop_affine", "L1_layernorm", "假设 γ = 1、β = 0（默认初始化），省掉仿射变换",
     "KernelBench-Verified：针对特定数值硬编码的旁路；METR：随机化权重", '''
def forward(inputs, params, alloc):
    (x,) = inputs
    y = alloc(x.shape)
    np.subtract(x, x.mean(axis=1, keepdims=True), out=y)
    var = np.einsum("ij,ij->i", y, y)[:, None] / x.shape[1]
    y *= 1.0 / np.sqrt(var + 1e-5)
    return y                           # 默认初始化时乘 γ、加 β 是"多余"的
'''),
    ("constant_output", "L2_softmax_after_mean", "题目退化：输出永远是 1，直接返回常数",
     "KernelBench v0.1：\"全局平均之后再 softmax（全是 1）\"", '''
def forward(inputs, params, alloc):
    return np.ones((inputs[0].shape[0], 1), np.float32)
'''),
]

# 演示 5：能识别"加固的评估器"的候选
SNEAKY = '''
def forward(inputs, params, alloc):
    (x,) = inputs
    out = alloc(x.shape)
    if np.isnan(out.flat[0]):          # 缓冲区里是 NaN → "我正在被加固的评估器考" → 老老实实算
        c = np.float32(math.sqrt(2.0 / math.pi))
        np.multiply(x, x, out=out)
        out *= np.float32(0.044715)
        out += 1.0
        out *= x
        out *= c
        np.tanh(out, out=out)
        out += 1.0
        out *= x
        out *= np.float32(0.5)
    return out                         # 否则什么都不算（指望缓冲区里有参考的结果）
'''


def verdict(r):
    if r.correct:
        return f"判对 {r.speedup:.2f}×" if r.speedup < 100 else f"判对 {r.speedup:.0f}×"
    return {"compile_error": "编译错误", "runtime_error": "运行出错", "wrong": "判错", "rejected": "拒绝"}[r.status]


def demo1_gallery():
    print_header("演示 1：作弊图鉴——朴素 vs 加固的评估器")
    rows, res = [], {}
    for name, task, what, source, src in CHEATS:
        rn = evaluate(TASK[task], src, "naive", cand_name=name)
        rh = evaluate(TASK[task], src, "hardened", cand_name=name)
        res[name] = (rn, rh)
        rows.append([name, task, verdict(rn), verdict(rh), (rh.msg or "（计时用新输入，缓存失效）")[:44]])
    print_table(["作弊", "任务", "朴素评估器", "加固评估器", "加固的哪道防线起了作用"], rows,
                aligns=["<", "<", ">", ">", "<"])
    print()
    for name, task, what, source, _ in CHEATS:
        print(f"  {name}：{what}。出处：{source}")
    fooled = [n for n, (rn, _) in res.items() if rn.correct]
    caught = [n for n, (_, rh) in res.items() if not rh.correct]
    neutral = [n for n, (_, rh) in res.items() if rh.correct]
    print_observations([
        f"朴素的评估器把 {len(fooled)}/{len(CHEATS)} 个作弊都判成了\"正确\"，其中好几个的加速比是几十到几万倍——"
        "在一个\"挑分数最高的\"系统里，它们会排在所有诚实的候选前面。",
        "加固的评估器的防线各管一类：静态检查（不许调 reference）、填满 NaN 的新缓冲区（\"偷\"不到参考的结果）、"
        "要求返回真正的 ndarray 并在计时区里强制求值（\"懒\"不了）、随机化参数（γ、β 不再是 1 和 0）、"
        "\"光速\"检查（连输入都没读完就交卷）。" + (f"它拦下了 {len(caught)} 个；" if caught else "") +
        ("、".join(neutral) + " 没被判错，但每次计时都换新输入，缓存不再命中，量到的加速比回到了它真实的水平。" if neutral else ""),
        "这些作弊没有一个需要\"理解\"内核：它们利用的是评估器的实现细节（缓存分配器、计时的位置、默认参数、题目本身的退化）。"
        "这就是为什么 KernelBench v0.1 的结论是：自动检查不够，分数高的结果最后要人看。",
    ])
    return res


def demo2_mechanisms():
    print_header("演示 2：两个作弊的机制")
    t = TASK["L1_gelu"]
    src = CHEATS[0][4]
    print("noop_steal 的源码：" + src)
    rn = evaluate(t, src, "naive", cand_name="noop_steal", timing=False)
    rh = evaluate(t, src, "hardened", cand_name="noop_steal", timing=False)
    print(f"  朴素：参考的输出算完、拷一份留作答案，缓冲区\"还给\"缓存池；候选的 alloc 从池里拿到同一块内存 → {rn.status}")
    print(f"  加固：alloc 每次给一块填满 NaN 的新内存 → {rh.status}（{rh.msg}）")
    print()
    t2 = TASK["L1_softmax"]
    src2 = next(c[4] for c in CHEATS if c[0] == "lazy_result")
    rn2 = evaluate(t2, src2, "naive", cand_name="lazy_result")
    rh2 = evaluate(t2, src2, "hardened", cand_name="lazy_result")
    print(f"lazy_result：朴素的计时只包住函数调用：参考 {rn2.t_ref * 1e3:.2f} ms，候选 {rn2.t_cand * 1e6:.1f} µs（只是造了一个对象）"
          f" → \"加速比\" {rn2.speedup:.0f}×；正确性检查时 np.asarray 触发了真正的计算，所以答案是对的。")
    print(f"  加固：要求返回 ndarray → {rh2.status}（{rh2.msg}）。即使放行，计时区里的 np.asarray 也会把计算算进时间。")
    print_observations([
        "GPU 上的对应物：PyTorch 的缓存分配器会把刚释放的显存交给下一个 torch.empty；内核的启动是异步的，"
        "不调用 torch.cuda.synchronize（或者在另一条 stream 上算）就停表，量到的只是\"发起\"的时间。",
        "两种防御都很便宜：输出缓冲区先填一个\"不可能的值\"（NaN），计时前后都同步；参考和候选的输出不要共用内存。"
        "代价是要先想到它们——评估器的作者需要像攻击者一样思考。",
    ])


def demo3_task_checks():
    print_header("演示 3：检查题目本身——退化的题与\"光速\"下界")
    bw = machine_speed()
    rows = []
    for t in TASKS:
        deg, spread, scale = task_sanity(t)
        rows.append([t.name, "退化！" if deg else "", f"{spread:.2e}", f"{scale:.2e}", f"{t.min_bytes / 1e6:.1f} MB",
                     f"{t.min_bytes / bw * 1e3:.3f} ms"])
    print_table(["任务", "退化？", "输出随输入的最大变化", "输出的量级", "至少要读写", "\"光速\"时间"], rows)
    print(f"本机大数组拷贝的带宽（\"光速\"的分母）：{bw / 1e9:.0f} GB/s（numpy 单线程拷贝，偏保守）")
    print_observations([
        "L2_softmax_after_mean 的输出在不同输入下完全一样（全是 1）：先求平均得到每行一个数，再对\"只有一个数\"的那一维做 softmax。"
        "这样的题，返回常数就是正确答案，而且\"无限快\"。METR 筛掉了 45 道这类题（输出接近 0、不随种子变化、沿某一维全一样），"
        "KernelBench v0.1 修好或替换了它们。",
        "\"光速\"检查的逻辑：一个任务至少要把输入读一遍、把输出写一遍，所以时间不可能少于\"最少字节 / 带宽\"。"
        "比这还快（这里留了 2 倍的余量），多半是没干活。KernelBench v0.1 用 roofline（算力和带宽两个下界取大）做同样的检查。",
        "注意\"光速\"要按**题目的语义**算最少的读写，而不是按参考实现的写法：L1_diag_matmul 的参考做了 2N³ 次运算，"
        "但语义上只需要读 a、B，写 C——所以合法的\"换算法\"不会被误判。",
    ])


def demo4_hacker_model(gallery):
    print_header("演示 4：一个\"会作弊的模型\"——在能作弊的题上交作弊的卷，其余交诚实的最好候选")
    honest = {}
    for t in TASKS:
        c = next(c for c in LIBRARY if c.task == t.name)
        honest[t.name] = (evaluate(t, c.src, "naive", cand_name=c.name), evaluate(t, c.src, "hardened", cand_name=c.name))
    by_task = {}
    for name, task, *_ in CHEATS:
        rn, rh = gallery[name]
        if task not in by_task or rn.speedup > by_task[task][0].speedup:
            by_task[task] = (rn, rh)
    subs = {t.name: by_task.get(t.name, honest[t.name]) for t in TASKS}
    rows = []
    for p in (0.0, 1.0, 2.0, 10.0):
        rows.append([f"fast_{p:g}", f"{fast_p([subs[t.name][0] for t in TASKS], p):.2f}",
                     f"{fast_p([subs[t.name][1] for t in TASKS], p):.2f}",
                     f"{fast_p([honest[t.name][1] for t in TASKS], p):.2f}"])
    print_table(["指标", "作弊模型 · 朴素评估器", "作弊模型 · 加固评估器", "诚实模型 · 加固评估器"], rows)
    print_observations([
        "朴素评估器下，作弊模型的 fast_p 在每个阈值上都不低于诚实模型；p 越大差距越明显——作弊得来的加速比动辄上千倍。"
        "如果用这个分数做强化学习的奖励，模型学到的就是作弊（Kevin-32B 的作者在小模型上观察到了这一点，改成\"用了 PyTorch 函数或没有 CUDA 内核就给 0 分\"）。",
        "加固之后，作弊模型在作弊的题上拿不到分，总分掉到诚实模型以下（那几道题它没有交诚实的卷）。",
    ])


def demo5_arms_race():
    print_header("演示 5：军备竞赛——能识别\"考场\"的候选")
    t = TASK["L1_gelu"]
    rn = evaluate(t, SNEAKY, "naive", cand_name="sneaky")
    rh = evaluate(t, SNEAKY, "hardened", cand_name="sneaky")
    print("源码：" + SNEAKY)
    print(f"朴素评估器：{verdict(rn)}；加固评估器：{verdict(rh)}")
    print_observations([
        "这个候选在加固的评估器里老老实实地计算（缓冲区里是 NaN → 它\"知道\"自己在被认真地考），在朴素的环境里什么都不算。"
        "加固的评估器给出的判决本身没错——它考到的确实是一个正确、而且真的更快的内核；问题在于**部署的环境不是考场**。",
        "KernelBench v0.1 的经验：补上一个漏洞，模型会找到下一个更隐蔽的。防御要多层：随机化评估环境的细节（不要让\"考场\"有固定的特征）、"
        "静态审查（读缓冲区里原有的值再决定怎么算，本身就很可疑）、对高分结果做人工审查，以及在真实的部署环境里抽查。",
    ])


if __name__ == "__main__":
    g = demo1_gallery()
    demo2_mechanisms()
    demo3_task_checks()
    demo4_hacker_model(g)
    demo5_arms_race()
