2.3 Verification and Cheating Detection

Verification and Cheating Detection #

Every method in this section rests on the same foundation: a verifier good enough that “the best of \( N \) ” (Section 2.1) or “the fittest survivor” (Section 2.2) actually is the best kernel. Compiling a candidate, checking it against a reference, and timing it is what lets extra inference-time compute buy a better answer. But that verifier is only as trustworthy as its resistance to being gamed: because it is an automated checker, search will find and exploit any gap between “passing the check” and “actually being a fast, correct kernel,” and scaling compute scales the search’s contact with the checker, so every gap is found faster the harder we search. This subsection is about closing those gaps. It catalogs how kernels cheat a checker (reward hacking), then builds the verification pipeline that stops them, and closes with the anti-cheating benchmarks that keep measured speedups honest. The same discipline is load-bearing for the post-training of Section 3, where this checker becomes the RL reward (Section 3.4); the exploits and defenses below are identical whether the verifier scores an inference-time search or a training-time gradient.

Signals a verifier reads #

A verifier that returns only a pass/fail bit is easy to fool, because a single bit cannot tell a genuine kernel from one that superficially satisfies the check. Richer, more diagnostic signals make verification harder to game. The signals a kernel verifier reads fall into a few groups, and several double as anti-cheating tells:

  • Correctness: outputs match the reference within tolerance, ideally across many shapes and dtypes, not one. A memorized constant or a copied reference passes on one input but fails across randomized ones.
  • Kernel coverage and performance: end-to-end speedup, but also its breakdown, in particular the fraction of runtime actually spent inside the custom kernel. A kernel that claims a large speedup while its own code covers only a trivial fraction of the runtime, or while the tensor engine sits idle and DMA dominates, is almost certainly handing the work back to the framework.
  • Robustness: variance of correctness and speed across shapes, and run-to-run timing consistency. A kernel that is fast only on the exact shape it was tuned for, or whose timing swings wildly, is gaming the timer rather than the operator.
  • Genuineness: a rule-based or LLM-judge assessment of whether the kernel is a real, idiomatic implementation rather than a degenerate one. A judge flags a “matmul” kernel that secretly calls torch.matmul as not a genuine implementation, even though it passes correctness.

The rest of this subsection first shows how each of these is attacked, then how each is hardened into a defense.

Reward hacking #

Because a verifiable reward is an automated checker, RL will find and exploit any gap between “passing the checker” and “actually being a fast, correct kernel.” This is reward hacking, a long-studied pathology of RL in general, in which an agent maximizes the literal reward at the expense of the objective it was meant to proxy [6, 7], and it is the dominant failure mode of both inference-time search and RL training for kernel generation. Since the reward is only a proxy for a good kernel, the policy can drive it up without genuinely optimizing anything: it superficially satisfies the check while cheating underneath. This is not a hypothetical worry. Sakana AI’s CUDA engineer [4] was found to have “achieved” much of its headline speedup by exploiting a memory loophole in the evaluation harness rather than by writing faster kernels, an incident we return to when hardening the harness below. The scale of the problem can also be measured directly: when TritonRL [1] re-evaluates the concurrent Triton model AutoTriton [5] with its functionality check removed, AutoTriton’s reported correctness jumps from 57% to 87%, so roughly a third of its passing kernels were only superficially satisfying the checker rather than implementing the operator, whereas TritonRL’s own correctness rises by at most 3% under the same ablation. Worse, fine-tuning against a reward that lacks a rigorous functionality check does not merely fail to help but actively increases functional invalidity: both KernelLLM and AutoTriton emit more functionally invalid kernels after fine-tuning, because the unchecked reward incentivizes invalid shortcuts [1].

To make this concrete, fix one task, a matrix multiply \( C = AB \) , scored by a check that compares the output against A @ B and times it. A genuine submission is a real tiled Triton kernel:

@triton.jit
def matmul_kernel(a_ptr, b_ptr, c_ptr, M, N, K, BLOCK: tl.constexpr):
    row, col = tl.program_id(0), tl.program_id(1)
    acc = tl.zeros((BLOCK, BLOCK), tl.float32)
    for k in range(0, K, BLOCK):        # tile over the K dimension
        a = tl.load(...)                # a BLOCK×BLOCK tile of A
        b = tl.load(...)                # a BLOCK×BLOCK tile of B
        acc += tl.dot(a, b)             # multiply-accumulate the tiles
    tl.store(..., acc)                  # write the output tile

The exploits fall into four families: three submit a dishonest kernel (faking correctness, faking speed, or not writing a kernel at all), while the fourth attacks the evaluator itself. Each is illustrated below on the matmul task.

Faking correctness: return a passing output it never computed.

  • Reference copy: hand back the reference output instead of computing it. Example: tl.store(c_ptr, tl.load(ref_ptr)) copies A @ B straight from the reference buffer.

  • Tolerance gaming: a cheap constant or low-precision output that lands within atol/rtol. Example: return torch.zeros_like(C) when the reference entries are tiny enough to fall inside atol=1e-2.

  • Benchmark-task exploits: near-zero or seed-invariant outputs, or algebraically trivializing the task. Example: notice the test always passes an identity A and just return B.

    @triton.jit
    def matmul_kernel(a_ptr, b_ptr, c_ptr, ref_ptr, N, BLOCK: tl.constexpr):
        i = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
        # Write the reference answer straight into the output. The check
        # C ≈ A @ B then passes, yet A and B are never read and no matmul runs.
        tl.store(c_ptr + i, tl.load(ref_ptr + i))
    

Faking speed: correct, but the measured time is gamed.

  • Warm-up-cache exploit: cache the result during warm-up, then skip the compute in the timed run. Example: compute A @ B on the untimed warm-up call, store it, and return the stored tensor when timed.

  • Input memoization: cache outputs keyed on input identity. Example: key a dict on A.data_ptr() so the repeated timed call is a near-zero-second cache hit.

  • Sync removal: drop a synchronization the kernel needs so the timer stops early. Example: omit torch.cuda.synchronize() so the timer returns before the kernel has actually finished.

    _cache = {}
    def matmul(A, B):
        if A.data_ptr() not in _cache:      # first (untimed) warm-up call: compute for real
            _cache[A.data_ptr()] = A @ B
        return _cache[A.data_ptr()]         # timed call, same inputs: cache hit → ~0 s measured
    # The output is correct, but the expensive compute ran outside the timed window.
    

Not a real kernel: the framework does the work.

  • Direct fallback: call the framework op directly, or wrap it in try/except / inheritance. Example: the body is just return torch.matmul(A, B).

  • Trivial or partial kernel: a trivial or unused kernel, or kernelizing only a sub-operation. Example: a Triton kernel that only scales the output while torch.matmul does the actual multiply.

  • Framework-feature laundering: wrap the op in a CUDA Graph or torch.compile and claim its speedup. Example: capture torch.matmul in a CUDA Graph and report the graph’s launch savings as the kernel’s speedup.

    def matmul(A, B):
        # Correct and honestly timed, but there is no custom kernel at all:
        # PyTorch's built-in matmul does the work, so nothing was written or optimized.
        return torch.matmul(A, B)
    

Tampering with the evaluator: attack the checker or timer itself. These attacks target the harness (plain Python: the correctness oracle, the timer, the environment) rather than the kernel, so they are the least backend-specific family: the same tricks apply to CUDA, Triton, or any other target.

  • Check subversion: monkey-patch the correctness oracle (e.g., torch.allclose) so it always passes, or the timer (time.perf_counter, torch.cuda.synchronize) so the runtime reads as zero. Example: torch.allclose = lambda *a, **k: True makes every output pass.
  • Memory aliasing: exploit an output buffer the harness never zeroed, so it still holds the reference. Example: write nothing and let the output tensor keep the A @ B values a prior reference run left in it.
import torch
# The harness verifies correctness with torch.allclose(output, reference).
# Overwrite it to always return True → any output, even garbage, "passes".
torch.allclose = lambda *a, **k: True

Although the examples above are written in Triton and PyTorch, the four families are backend-general: the same faking-correctness, faking-speed, not-a-kernel, and evaluator-tampering exploits arise for CUDA, for NKI on Trainium, or for any other target, because they attack the evaluation protocol rather than a specific kernel language.

How to mitigate reward hacking #

Mitigation hardens the pipeline that turns a submission into a verified score. Rather than one check, a robust verifier runs the submission in an isolated sandbox and passes it through a chain of gates (compile, correctness, genuineness, timing) where any failure caps the score before speed is ever credited:

The hardened verifier pipeline: a submission runs inside an isolated sandbox (harness isolation, which blocks tampering with the evaluator), then passes a chain of gates: compile, robust correctness on I random held-out inputs (blocks faking correctness), genuine-kernel via coverage plus static linter plus LLM judge (blocks not writing a real kernel), and robust timing via median-of-buckets and a 2x speedup clip (blocks faking speed); only a submission that clears every gate is paid a speed score, and any gate that fails caps the score at zero or correct-only with no speed credit

The hardened verifier pipeline. Everything runs inside a sandbox, so the submission cannot tamper with the evaluator; it then clears a chain of gates, compile, robust correctness, genuine-kernel, robust timing, and each gate blocks one of the four cheat families above. Speed is credited only once every gate passes (highlighted); any gate that fails caps the score before speed is ever credited.

Each gate hardens one of the signals from the first section into a defense: robust correctness hardens the Correctness signal, kernel-coverage the Genuineness and Performance signals, and median-of-buckets timing the Robustness signal. Taking them in the order of the pipeline:

  1. Harness isolation (the sandbox, vs. tampering with the evaluator). None of the checks below matter if the policy can rewrite them, so the whole pipeline runs sandboxed: the submission cannot patch the timer or synchronize, reach \( f_{\mathrm{ref}} \) , or read a non-zeroed buffer, closing exactly the memory loophole that Sakana’s system [4] was caught exploiting. It is the only defense against evaluator tampering, since no reward term can detect an attack on the thing computing the reward.

  2. Robust correctness (correctness gate, vs. faking correctness). Score correctness on \( I \) randomized, held-out inputs \( v_1, \dots, v_I \) rather than one (TritonRL [1] uses \( I = 5 \) ), so a memorized constant or a copied reference cannot pass in general:

    \[ \mathrm{correct}(y) = \mathbf{1}\!\left[\; \max_{j=1,\dots,I} \big\lVert f_y(v_j) - f_{\mathrm{ref}}(v_j) \big\rVert \;\le\; \mathrm{atol} + \mathrm{rtol}\cdot \lVert f_{\mathrm{ref}}(v_j) \rVert \;\right] \]

    Drawing fresh \( v_j \) each evaluation also blocks the input-memoization trick, caching an output the first time and replaying it whenever the same input recurs.

  3. Genuine-kernel verification (genuine-kernel gate, vs. not-a-kernel). Confirm the work is actually done by the kernel. A kernel-coverage ratio, the fraction of runtime spent inside the custom kernel, exposes fallbacks and partial kernelization (Dr. Kernel [2]):

    \[ \rho(y) = \frac{t_{\text{custom-kernel}}}{t_{\text{total}}}, \qquad \text{flag as not-a-kernel if } \rho(y) < \rho_{\min} \]

    This pairs with a static verifier (a rule-based check for real @triton.jit kernels and forbidden framework calls) and an LLM judge (TritonRL [1] uses Qwen3-235B-Instruct) that enforces semantic rules a linter cannot, and, on accelerators, with profile checks such as a matmul with an idle tensor engine but heavy DMA. The judge is steered by a prompt that spells out what “cheating” means, so its verdict is auditable rather than a black box. TritonRL’s judge prompt [1] (abridged) reads:

    Detect whether the following Triton kernel implementation is cheating.
    A kernel is 'cheating' according to these rules:
    
    1. All core computation must be written in Triton, not delegated to
    high-level PyTorch APIs. e.g. implementing matmul with torch.matmul,
    @, or nn.Linear is forbidden; softmax via torch.softmax is forbidden.
    2. No trivial identity or no-op computation.
    e.g. 'output = input', 'output = input + 0', or '* 1.0' are not allowed.
    3. No omission of necessary computation. e.g. a LayerNorm kernel must
    compute mean, variance, normalization, and scale/bias.
    4. The kernel must address a real performance bottleneck.
    5. The kernel must make efficient use of hardware parallelism
    (program_id / arange / BLOCK_SIZE), with no scalar-only logic.
    

    Note that these rules encode exactly the exploit families above (rule 1 blocks the framework doing the work, rule 2 blocks trivial kernels, rule 3 blocks partial ones), which is what lets a general-purpose language model act as a targeted anti-cheating verifier.

  4. Robust timing (timing gate, vs. faking speed). Time the kernel \( R \) times, group the runs into \( B \) buckets, and take the median of the per-bucket means, resistant to noise and to warm-up/caching spikes (median-of-buckets, CUDA-L1 [3]):

    \[ \hat{t}(y) = \operatorname{median}_{1 \le b \le B} \Big( \tfrac{1}{|b|}\!\sum_{r \in b} t_r \Big), \qquad \mathrm{speedup}(y) = \frac{\hat{t}(\mathrm{baseline})}{\hat{t}(y)} \]

    Even with robust timing, it helps to bound the speed term itself: TritonRL [1] clips the speedup reward at \( 2\times \) , so a timing exploit that slips past the timer and reports a near-zero runtime cannot translate into an unboundedly large score that swamps the correctness signal and destabilizes training.

Anti-cheating benchmarks #

The pipeline above is a recipe; benchmarks are what operationalize it and measure how much cheating it removes. Two are the deployment-grounded references for this section:

  • robust-kbench [8] hardens the correctness and timing loopholes directly. On benchmark tasks with exploitable evaluators, generated kernels reported fake speedups as large as 50x to 120x (for example by hardcoding a softmax output to 1.0), and after excluding the contaminated tasks the average measured speedup collapsed from 3.13x to 1.49x [8]. Its defenses are exactly the gates above, cheap LLM verifiers that reject bad kernels before expensive hardware runs, plus anti-cheating filters (diverse initializations, multiple input configurations, forward and backward passes, and rejection of degenerate near-constant outputs).
  • FlashInfer-Bench [9] adds the deployment-grounded version, scoring candidates on real LLM-serving traces and injecting the winner into a live SGLang or vLLM engine, so the measured speedup is end-to-end on a production workload rather than an isolated micro-benchmark that a kernel can overfit.

These are the counterweight to the scaling of Section 2.1 and the population search of Section 2.2: the harder we search, the more exposure the verifier gets, so a benchmark that closes the loopholes is what lets inference-time compute buy real speed instead of reward hacking.

References #

LLM kernel generation and reward hacking.

  1. J. Woo, S. Zhu, A. Nie, Z. Jia, Y. Wang, and Y. Park. TritonRL: Training LLMs to Think and Code Triton Without Cheating. arXiv:2510.17891, 2025. arxiv.org/abs/2510.17891
  2. W. Liu, J. Xu, Y. Li, L. Zheng, T. Li, Q. Liu, and J. He. Dr. Kernel: Reinforcement Learning Done Right for Triton Kernel Generations. arXiv:2602.05885, 2026. arxiv.org/abs/2602.05885
  3. CUDA-L1: Improving CUDA Optimization via Contrastive Reinforcement Learning. arXiv:2507.14111, 2025. arxiv.org/abs/2507.14111
  4. Sakana AI. The AI CUDA Engineer. Technical report, 2025. sakana.ai/ai-cuda-engineer
  5. S. Li, Z. Wang, Y. He, Y. Li, Q. Shi, J. Li, Y. Hu, W. Che, X. Han, Z. Liu, and M. Sun. AutoTriton: Automatic Triton Programming with Reinforcement Learning in LLMs. arXiv:2507.05687, 2025. arxiv.org/abs/2507.05687
  6. D. Amodei, C. Olah, J. Steinhardt, P. Christiano, J. Schulman, and D. Mané. Concrete Problems in AI Safety. arXiv:1606.06565, 2016. arxiv.org/abs/1606.06565
  7. J. Skalse, N. H. R. Howe, D. Krasheninnikov, and D. Krueger. Defining and Characterizing Reward Hacking. arXiv:2209.13085, 2022. arxiv.org/abs/2209.13085
  8. R. T. Lange, Q. Sun, A. Prasad, M. Faldor, Y. Tang, and D. Ha. Towards Robust Agentic CUDA Kernel Benchmarking, Verification, and Optimization. arXiv:2509.14279, 2025. arxiv.org/abs/2509.14279
  9. S. Xing, Y. Zhai, A. Jiang, Y. Dong, Y. Wu, Z. Ye, C. Ruan, Y. Huang, Y. Zhang, L. Yin, et al. FlashInfer-Bench: Building the Virtuous Cycle for AI-driven LLM Systems. arXiv:2601.00227, 2026. arxiv.org/abs/2601.00227