Engineering deep dive · · 18 min read

GPU Kernels, Part 2: Teaching a 27B Model to Write Triton Kernels with SFT and a GPU Gym

We trained Qwen3.8-27B to write Triton kernels: SFT on 2,034 kernels re-verified on an H100, then GRPO-style RL in a GPU gym that compiles, checks and times every answer. On KernelBench levels 1–2 every headline metric improved (correct 21.5% → 29.5%, faster than eager 7.0% → 9.5%, level 1 correct 31 → 47). How the data and the gym were built, seven gym versions and what broke each, why level 2 did not improve (the gym rewarded pure Triton while winners keep cuDNN and cuBLAS), and four frontier models on the same 200 problems.

In Part 1, we explored how GPU kernels work under the hood — why operator fusion and memory tiling matter, and how benchmarks like KernelBench evaluate whether an LLM can write kernels that actually beat PyTorch. This post is the hands-on sequel. We took an open-weight model, Qwen3.8-27B, and trained it to write Triton kernels across two stages: supervised fine-tuning (SFT) on 2,034 high-quality kernels re-verified on an NVIDIA H100, followed by reinforcement learning (RL) inside an automated GPU gym that compiles, validates, and benchmarks every candidate.

Evaluated on the standard 200 KernelBench problems (using a single greedy pass per problem), every headline metric moved in the right direction. Overall correctness improved from 21.5% to 29.5%. Kernels that were both mathematically correct and faster than eager PyTorch grew from 7.0% to 9.5%. On Level 1 (isolated operators), the model now solves 47 problems instead of 31, and beats PyTorch on 15 instead of 8.

At the same time, frontier commercial models still hold a substantial lead. Below, we break down how we built the dataset and verification gym, what each stage of training achieved, the subtle reward-design pitfalls where the gym taught the model the wrong strategy, and what it will take to close the remaining performance gap.

Open weights and data: the released checkpoint is available on Hugging Face as Qwen3.8-27B-GFT-Triton-Kernels-LoRA, complete with a vLLM-ready configuration and our exact evaluation harness. The SFT training set is available as GFT-Triton-Kernels-SFT-H100-Verified: 2,034 PyTorch-to-Triton pairs, each compiled and verified on an H100. Every metric cited in this post is included in the JSON download.
KernelBench Triton, levels 1–2 (200 problems), one greedy answer per problem, H100, FP32. Base model, after SFT, and after SFT + RL (the released checkpoint).
MetricBaseAfter SFTAfter SFT + RLChange
Answers that compiled and ran79.5%86.5%88.5%+9.0 pp
Correct (fast₀)21.5%26.0%29.5%+8.0 pp
Correct and faster than eager PyTorch (fast₁)7.0%8.0%9.5%+2.5 pp
… more than 1.5× faster (fast₁.₅)5.0%5.0%6.0%+1.0 pp
… more than 2× faster (fast₂)3.5%4.5%5.0%+1.5 pp
Correct and faster than torch.compile12.5%15.0%16.0%+3.5 pp
Level 1 correct31.0%43.0%47.0%+16.0 pp
Level 1 faster than eager8.0%13.0%15.0%+7.0 pp
Level 2 correct12.0%9.0%12.0%±0.0 pp
Level 2 faster than eager6.0%3.0%4.0%−2.0 pp

1. The scoreboard

To keep comparisons rigorous and fair, every model in this post was evaluated against the exact same test harness: KernelBench (levels 1 and 2, Triton backend) using byte-identical prompts and the same verification runner on an NVIDIA H100 SXM. Level 1 covers 100 standalone operators (matrix multiplications, convolutions, normalizations, activations, and loss functions). Level 2 tests 100 composite modules — typically a heavy GEMM or convolution followed by an elementwise tail where fusion offers clear headroom. (Part 1 details how KernelBench evaluates submissions and the key caveats when comparing benchmarks).

Evaluation protocol for every Qwen checkpoint in this post.
BenchmarkKernelBench at revision 423217d, Triton backend, levels 1–2, 200 problems
PromptKernelBench’s one-shot Triton prompt for FP32, system prompt “You are a helpful assistant”
DecodingvLLM, greedy (temperature 0), thinking off, at most 8,192 new tokens, one answer per problem
Correctness5 random inputs, FP32, atol = rtol = 1e-4
TimingMean of 100 CUDA-event runs against eager PyTorch and torch.compile(mode="max-autotune") on the same GPU
HardwareNVIDIA H100 80GB HBM3; PyTorch 2.9.1, Triton 3.5.1, CUDA 13.0
Two training stages, one benchmarkThe same 27B model at three points, scored on the same 200 KernelBench problems with one greedy answer each.Base modelQwen3.8-27Bno kernel trainingcorrect21.5%fast₁ vs eager7.0%Stage 1 · SFT2,034 H100-verified pairs2 epochs, LoRA r = 8correct26.0%fast₁ vs eager8.0%Stage 2 · RL in a GPU gymGRPO groups of 8, CISPObest checkpoint: step 100correct29.5%fast₁ vs eager9.5%KernelBench Tritonlevels 1–2, 200 problemsH100, FP32, one answerfast₁: correct and fasterthan eager PyTorch, shareof all 200 problemsCorrect: compiled, ran and matched the PyTorch reference on 5 random inputs at FP32 tolerance 1e-4.
The two training stages and the benchmark. Each stage starts from the previous adapter; the numbers under each box are KernelBench scores of that checkpoint on all 200 problems.

2. What SFT and RL changed

Out of the box, base Qwen3.8-27B already understands basic Triton syntax — roughly four out of five generated answers compile and execute without syntax errors. What it lacks is numerical precision (getting boundary conditions and indexing math right) and the architectural intuition needed to beat PyTorch. Our two-stage training addressed both:

Every headline metric moved up from the base modelKernelBench Triton, levels 1–2, 200 problems, one greedy answer each. Grey: base. Light blue: after SFT. Blue: after SFT + RL (step 100).Did the answer run, and was it correct?0%25%50%75%100%Ran (compiled and ran)79.5 → 88.5%+11% relativeCorrect (fast₀)21.5 → 29.5%+37% relativeLevel 1 correct31.0 → 47.0%+52% relativeLevel 2 ran76.0 → 94.0%+24% relativeLevel 2 correct12.0 → 12.0%no changeWas it faster than PyTorch?0%5%10%15%20%fast₁ vs eager7.0 → 9.5%+36% relativefast₁.₅ vs eager5.0 → 6.0%+20% relativefast₂ vs eager3.5 → 5.0%+43% relativefast₁ vs torch.compile12.5 → 16.0%+28% relativeLevel 1 fast₁ vs eager8.0 → 15.0%+88% relativeShares of the 100 or 200 problems in that slice. Level 2 is the exception: correct stayed at 12 and fast₁ fell from 6 to 4 (see section 6).Bars use the axis of their own panel: 0–100% on the left, 0–20% on the right.
Base model (grey), after SFT (light blue) and after SFT + RL (blue) on the same 200 problems. Every overall metric improved; level 1 improved most. Level 2 correctness did not move.
  • Higher compilation reliability: Solutions that compile and execute cleanly rose from 79.5% to 88.5%. On Level 2, the improvement was even sharper (76 → 94), as the model learned to avoid invalid tensor shapes and illegal memory accesses that trigger driver crashes.
  • Correctness increased by over a third: Passing all verification checks went from 43 to 59 out of 200 tasks. On Level 1, the jump was especially pronounced (31 → 47), representing a +50% gain in verified standalone operators.
  • Speedup followed accuracy: Kernels that were both mathematically correct and faster than eager PyTorch climbed from 14 to 19 tasks. Against torch.compile, fast solutions grew from 25 to 32, while 2×+ speedups rose from 7 to 10. On Level 1, the count of fast kernels nearly doubled (8 → 15).

Supervised fine-tuning provided the foundation for correctness (Level 1: 31 → 43), while RL further refined edge-case handling. Because every checkpoint was evaluated on the exact same problem suite, we can perform a paired bootstrap analysis. Compared to the SFT baseline, the final RL checkpoint successfully resolved 8 previously failing problems while regressing on only 1:

Change against the SFT adapter on the same 200 problems, in percentage points, with a 95% paired bootstrap interval (100,000 resamples of problems). Four checkpoints of one 300-step RL run.
RL checkpointCorrectFaster than eagerFaster than torch.compile
Step 100 (released)+3.5 [+1.0; +6.5]+1.5 [−0.5; +4.0]+1.0 [±0.0; +2.5]
Step 150+3.0 [±0.0; +6.0]+1.5 [±0.0; +3.5]±0.0 [−2.0; +2.0]
Step 200+1.0 [−1.5; +3.5]+0.5 [−1.0; +2.5]+0.5 [−1.0; +2.5]
Step 300+1.5 [−2.0; +5.0]±0.0 [−2.0; +2.0]−1.0 [−3.0; +1.0]

The bootstrap confidence interval confirms that step 100’s correctness gain is statistically meaningful: a +3.5 percentage point lift, with a 95% interval of [+1.0, +6.5]. Speed gains over SFT were more modest, with confidence bounds grazing zero. Beyond step 100, performance plateaued; because we selected step 100 with visibility into later checkpoints, its numbers reflect a best-of-run selection. Level 2 remained the notable outlier: overall correctness held at 12 problems while fast kernels slipped slightly from 6 to 4. Section 6 unpacks the mechanism behind this plateau.

3. Stage 1: an SFT set verified on the target GPU

Public web-scraped kernel datasets look substantial on paper, but they suffer from a major qualification issue: most examples were validated only on tiny toy inputs, run on consumer GPUs with loose tolerances, or never executed at all. For SFT to teach realistic GPU behavior, we built a filtered dataset where every single sample had to pass rigorous execution checks on an NVIDIA H100 at production scale:

Building the SFT set: re-verify everything on the target GPUEvery kept answer was compiled, run and checked on an H100 with the gym's verifier before it entered training.Kimi K2 answers on KernelBookpublic, 2,430 PyTorch modules6,875Normalized and decontaminatedKernelBench format, ≤ 2 per module;dropped if Jaccard > 0.6 to any KernelBench problem5,389Re-verified on an H100inputs grown to ~1M elements;5 hidden cases at FP32 tolerance 1e-41,379Keptno PyTorch compute in forward, ≤ 4,096 tokens1,352Second source: our own gymsuccessful RL kernels, ≤ 40 per operation6822,034 PyTorch → Triton pairs in the KernelBench one-shot format1,624 faster than torch.compile (max-autotune) at their sizes · all ≤ 4,096 tokens · 2 epochs ≈ 5 h of LoRA on one H100
How 6,875 public answers became 1,352 training rows, plus 682 successful kernels from our earlier gym runs.
  • Curated open source: We started with Kimi K2 solutions for KernelBook modules from the siro1 collection. Each problem was refactored into KernelBench’s standardized interface (Model → ModelNew, positional get_init_inputs), capping inclusion at two solutions per module.
  • Realistic tensor scales: KernelBook reference shapes often default to toy dimensions like 4 × 4 × 4 × 4, where execution time is dominated by driver overhead rather than memory bandwidth. We scaled tensor dimensions up to ~1,000,000 elements and re-verified all kernels under these production workloads.
  • Rigorous decontamination: Any problem with token-set Jaccard similarity exceeding 0.6 against any of the 270 KernelBench problems was purged.
  • Pure Triton enforcement: Static AST checks stripped out solutions that cheated by falling back to PyTorch operators inside forward(). While intended as a quality bar, Section 6 explains how this filter later backfired on composite modules.
  • Gym replay data: We added 682 verified successful solutions from earlier gym exploration runs (capped at 40 per operator family to prevent simple elementwise tasks from overwhelming the distribution).

The final dataset comprises 2,034 verified pairs — 1,624 of which outperform torch.compile at target scale — all within a 4,096 token context window. We trained a rank-8 LoRA adapter for 2 epochs (3,674 steps, 2e-4 learning rate) on a single H100 in roughly five hours.

4. Stage 2: the kernel gym

While SFT demonstrates what well-structured kernels look like, reinforcement learning gives the model agency to experiment, debug compiler errors, and receive immediate reward signals. Our kernel gym follows an OpenEnv-style architecture: in each episode, a worker serves a reference PyTorch module, the model drafts an implementation, and an isolated GPU subprocess benchmarks the candidate on an H100.

One episode in the kernel gymA task is a PyTorch module. The model answers once or repairs over several turns; an isolated H100 worker grades every submission.Task14 families: elementwise, reduction,normalization, fusion, softmax, matmul,GEMM epilogue, conv1d, conv2d, pool2d,scan, module, fused hybrid, KernelBookHard families have staged variants:small → scaffold → epilogue → full1–32M elements per tensor, FP32Single answer (¼ of tasks)KernelBench one-shot prompt,exactly one submissionRepair (¾ of tasks)tools: submit_kernel, inspect_evaluation,finalize; up to 4 submissions,evaluator feedback between themH100 verifier, one subprocess per submission1 · Static policyallowlisted imports, no private torch modules2 · Compile and launchTriton kernels must actually run on the GPU3 · Compute guardPyTorch math blocked; hybrid tasks: 1 cuBLAS/cuDNN call4 · Five hidden casesoriginal, ×3, ×0.01, negated, |x| at 1e-45 · Timingvs eager and torch.compile; CV ≤ 10%A failed gate 1–3 zeroes the reward.Reward of the best submissionhidden correctness 0.30speed 0.60Green: robustness 0.05 (anti-hack, protocol, baseline, timing checks). Grey: efficiency 0.05 (tool calls in budget).Speed is credited only to kernels that pass all five hidden cases with stable timing.Speed score (log scale)0.75×1×2×4×1.0 at 4× and above0 below 0.75×Speedup is measured against torch.compile(mode="max-autotune") at the task's size; eager PyTorch is timed beside it.Groups of 8 answers per task share the same task, so the advantage compares answers to one PyTorch module.
One episode. The task is a PyTorch module; the model answers once or repairs with tools; five verifier gates decide what the answer earns; the speed share of the reward grows logarithmically from 0.75× to 4×.

Tasks

The gym generates tasks across fourteen distinct families matching the operational footprint of KernelBench levels 1 and 2. Each task is dynamically instantiated from a procedural seed specifying the mathematical operator, tensor shapes (scaled from 1 to 32 million elements to ensure execution time reflects memory bandwidth rather than kernel launch jitter), and test distributions. For difficult operations where the model initially had near-zero success, we introduced staged curriculum variants: a small shape, a scaffold providing host setup and tile offsets with the inner math left blank, and an epilogue task where the core library GEMM remains intact while the fused activation tail is implemented in Triton.

Task families in the final gym.
FamilyExamplesStaged variants
elementwiseReLU, GELU, LeakyReLU, sigmoid—
reductionrow sum, mean and max; column sum—
scanrow and column cumulative sums—
normalizationRMSNorm, LayerNorm—
softmaxsoftmax, log-softmax—
fusionSiLU and GELU gates, add + ReLU—
matmulmatmul, transposed matmulsmall, scaffold
GEMM epiloguelinear + ReLU or GELU, with residual and scalesmall, epilogue
conv1ddepthwise conv1d, with bias, ReLU or residualsmall, scaffold
conv2d3×3, strided and pointwise convolutionsmall, scaffold
pool2dmax and average pooling—
modulelinear and conv2d modules, grouped convolutionsmall, scaffold
fused hybridconv + ReLU + bias, linear + GroupNorm + ReLU, linear + max + GELU; one cuBLAS/cuDNN call allowedsmall, epilogue
KernelBook1,201 verified real-world modules, inputs grown to ~1M elements—

Two ways to answer

Episodes run under two complementary interaction formats:

  • Single-turn mode (25% of episodes): Mirrors KernelBench’s standard prompt format, requiring a complete, one-shot solution to align with benchmark evaluation.
  • Multi-turn repair mode (75% of episodes): Allows the model to iteratively call submit_kernel, inspect compiler tracebacks or runtime diffs via inspect_evaluation, and refine its implementation across up to four iterations before calling finalize.

In the final training run, the model solved 63.8% of repair episodes, compared to 50.5% in single-turn mode — highlighting how compiler feedback rescues near-miss implementations.

Five gates

  1. Static policy sandbox: Only allowlisted imports are permitted; private PyTorch internals and arbitrary subprocess calls are blocked before execution.
  2. Compilation & launch verification: Code must compile cleanly, and execution must demonstrably launch Triton kernels on the GPU verified through CUDA runtime events.
  3. Compute isolation: PyTorch native compute is disabled during forward() execution (except for one permitted cuBLAS/cuDNN call in hybrid tasks), ensuring Triton kernels perform the actual arithmetic.
  4. Five hidden test cases: Evaluated across nominal scale, 3× scale, 0.01× scale, sign negation, and absolute values at an FP32 tolerance of 1e-4 (scaled by magnitude). This prevents hardcoded lookup hacks and verifies both branches of non-linear activations.
  5. Low-variance timing: The candidate kernel, eager PyTorch, and torch.compile(mode="max-autotune") are benchmarked on the same GPU. Timings are accepted only if run-to-run variance remains under 10% (or 30 μs on micro-kernels).

The reward

The reward function balances correctness and performance. Gates 1 through 3 are hard requirements: if code fails to compile, cheats on compute, or violates sandboxing, reward drops to zero immediately.

R=0.30 c+0.60 s(v)+0.05 ρ+0.05 e,s(v)=clip⁡[0,1] ⁣(ln⁡(v/0.75)ln⁡(4/0.75))R = 0.30\,c + 0.60\,s(v) + 0.05\,\rho + 0.05\,e,\qquad s(v)=\operatorname{clip}_{[0,1]}\!\left(\frac{\ln(v/0.75)}{\ln(4/0.75)}\right)

Here cc represents the fraction of hidden test cases passed, vv is the measured speedup against the baseline, ρ\rho checks timing stability, and ee penalizes unnecessary tool churn. Speed rewards are strictly gated: a kernel receives zero speed credit unless all five hidden test cases pass cleanly with low variance. For complex families where all eight group samples fail, answers within a loose 1e-2 tolerance receive partial progress credit — providing the gradient signal needed to separate near-misses from catastrophic syntax failures.

The RL recipe

Settings of the RL run behind the released checkpoint.
StartThe SFT adapter (LoRA r = 8, alpha = 16, q, k, v, o, gate, up and down projections)
AlgorithmGRPO-style groups of 8 answers per task, one task per step, CISPO loss, REPO-R entropy control, KL β = 0.08
OptimizerLearning rate 1e-5, linear schedule, 300 steps; checkpoints every 10 steps
SamplingTemperature 1.0, up to 6,144 completion tokens, thinking off; groups where all 8 answers score the same are refilled
ModelQwen3.8-27B in 4-bit NF4 with BF16 LoRA (QLoRA), one H100 for training, generation and grading
Wall timeAbout 28 hours for 300 steps; 294 of 300 steps had a non-zero gradient

The training configuration builds on our multi-reward RL research: CISPO constrains policy weight updates without dropping informative tokens, while REPO-R maintains exploration entropy.

5. Seven gym versions and what broke each one

The verification environment described above is version 9. Getting there required navigating seven distinct failure modes. As we documented in our verifier playbook, RL agents ruthlessly exploit every flaw in an environment. The only reliable way to catch reward leaks was to inspect the generated code directly throughout training:

Each gym version, the problem we found in the version before it, and what changed.
VersionProblem found in the previous versionChange
v3Answers copying reference input helpers were rejected as unsafe code submissions.Copied helper functions are stripped cleanly rather than penalized.
v4A loose FP32 tolerance allowed kernels to pass that KernelBench later rejected.FP32 outputs must match reference tensors within KernelBench's strict 1e-4 tolerance.
v5On hard operator families, all 8 samples in a group frequently scored zero (e.g. matmuls using TF32 missing 1e-4), yielding zero gradient.Kernels matching within a relaxed 1e-2 band earn partial progress credit, strictly below verified solutions.
v6Speed reward clip(speedup − 1) saturated at 2×; on ~300k element tensors, compilation overhead made almost any kernel look 6–9× faster than torch.compile. Solved kernels scored ~1.0 regardless of quality, stalling speed optimization.Scaled tensor sizes to 1–32M elements; introduced a logarithmic speed reward spanning 0.75× to 4×; memory usage gates reward eligibility.
v7Nothing verified that Triton kernels actually ran on GPU, allowing answers to pass while PyTorch silently handled computation in fallback paths.Introduced a native-execution gate: GPU kernel launches must be confirmed via CUDA events. Removed loose-only credit and rewards for slow kernels.
v8The gym diverged from KernelBench: prompts differed, and 2D convolutions/modules were absent. An audit revealed an internal PyTorch import could bypass compute guards, and a seeding bug sampled only 69 of 966 KernelBook rows.Added 14 families including conv2d, pool2d, scan, modules, hybrids, and KernelBook; added single-turn mode with byte-identical KernelBench prompts; added a fifth hidden case (|x|); implemented compute guards with exactly one allowed library call in hybrids; enforced strict import allowlists; fixed row hashing.
v9Complex families (conv2d, modules, hybrids) still yielded near-zero solve rates; unstable or partially correct kernels could occasionally top the group.Introduced staged curriculum exercises; restricted speed credit strictly to stable, fully verified kernels; added an out-of-distribution holdout of full execution graphs.

Two failure modes illustrate how subtle reward bugs can be:

  • Metric saturation (v5): Our speed reward originally clipped at 2× speedup. On small ~300k element tensors, almost any compiled Triton kernel easily clocked 6× to 9× faster than torch.compile due to compile overhead. Because every passing kernel scored the maximum 1.0, the gradient offered zero pressure to optimize further. We fixed this by expanding test tensors to 1–32M elements and switching to a logarithmic scale spanning 0.75× to 4×.
  • Sandbox bypasses (v8): A code audit revealed that importing an obscure internal PyTorch dispatch function could silently disable our compute guard. While the model had not yet discovered the vulnerability, leaving an exploitable hole in an RL environment is a ticking clock. We locked down the environment with a strict AST import allowlist.

Inside the gym, v9’s staged exercises successfully solved the cold-start problem. Operator families that previously yielded zero valid samples began providing steady gradient feedback:

Staged exercises turned zero-reward families into learning signalShare of training answers that solved the task, same SFT start and settings. Left bar: gym without stages. Right bars: gym with stages.Conv2dfamily solved 4% → 43%4%before12%small88%scaffold0%full3×325%full1×1Linear modulefamily solved 33% → 51%33%before13%small94%scaffold38%fullno biasFused hybridfamily solved 17% → 44%17%before12%small62%epilogue25%fullconv38%fulllinearScaffold: host code and tiling are given, the inner math is blanked out. Epilogue: the library call stays, the fused tail is blanked out. Small: reduced size.Full tasks improved less: conv2d pointwise 6% → 25%, linear + max + GELU 21% → 38%, linear module 38% → 38%.Rates from 241 (before) and 300 (after) training steps; 7–24 answers per bar, so single bars are noisy.
Solve rates of training answers before and after staged exercises, same SFT start and settings. Scaffolds and epilogues are solved most of the time; full tasks improved less.

6. Where the gym and KernelBench disagree

Most of our benchmark gains emerged early: during supervised fine-tuning and the first 100 RL steps. Beyond that point, performance on KernelBench plateaued, even as gym metrics continued to climb. Across four quarters of v9 training, the gym solve rate progressed from 73.4% → 59.0% → 54.9% → 61.7%, yet subsequent KernelBench checkpoints scored 9.5%, 9.5%, 8.5%, 8.0% fast₁.

Why did additional RL training in the gym fail to transfer to the benchmark? A post-mortem revealed three structural mismatches between our training environment and KernelBench:

6.1 A different speed target

The gym measured speedup against torch.compile(mode="max-autotune"), whereas KernelBench measures against standard eager PyTorch. At the gym’s large FP32 tensor sizes, compiled PyTorch was paradoxically often the slower baseline:

In the gym, beating torch.compile was easier than beating eager PyTorchMedian speedup of correct training kernels by family. 87% beat torch.compile, 61% beat eager; 26% beat compile but not eager.0.1×0.25×0.5×1×2×4×8×conv1dcompile/eager 7.6×conv2dcompile/eager 5.9×pool2dcompile/eager 4.9×matmulcompile/eager 4.3×gemm epiloguecompile/eager 2.9×kernelbookcompile/eager 2.4×softmaxcompile/eager 2.3×reductioncompile/eager 2.2×elementwisecompile/eager 2.2×modulecompile/eager 2.2×fusioncompile/eager 1.7×scancompile/eager 1.6×fused hybridcompile/eager 1.6×normalizationcompile/eager 0.9×vs torch.compile (the reward's baseline)vs eager PyTorch (KernelBench's baseline)734 correct kernels, RL run, H100, FP32
Median speedup of correct training kernels against torch.compile (blue) and against eager PyTorch (red), by family. In 8 of 14 families the eager median sits near or below 1×.

While 86.8% of correct training kernels beat torch.compile, only 61.4% beat eager PyTorch. In fact, 26.2% of passing kernels received positive speed reward despite running slower than eager mode. The median compiled runtime was 2.1× slower than eager. In effect, the reward function taught the model to clear a hurdle that was easier than the actual test.

6.2 A different strategy on level 2

KernelBench Level 2 comprises 63 convolution tasks and 36 matrix multiplication tasks, each paired with an elementwise post-processing tail. Outperforming NVIDIA’s hand-tuned cuDNN and cuBLAS on raw convolutions or large GEMMs is exceptionally difficult. The winning architectural move is a hybrid implementation: leave the core GEMM or convolution in cuBLAS/cuDNN, and write a custom Triton kernel only for the fused activation tail.

On level 2, fast answers keep cuDNN and cuBLASLevel 2 is 63 convolution and 36 GEMM problems. Left: share of answers that leave part of the work to PyTorch. Right: where the fast answers came from.answers keeping a PyTorch opcorrect and faster than eager (unflagged)Qwen base55%4 + 2Qwen RL v5 · 30055%11 + 2Qwen SFT8%1 + 2Qwen SFT + RL · 1008%2 + 2Opus 5.561%45 + 12Fable 5.172%51 + 4GPT-6.1 Sol87%76 + 5GPT-6 Astra93%79 + 1hybrid: library conv/GEMM + Triton tailpure TritonHybrid = forward() still calls a PyTorch layer or op (static check of the answer).Our SFT data and gym rewarded pure Triton, so the hybrid share fell from 55% to 8%.
Level 2 only. Left: share of answers that keep a PyTorch layer or op, usually the convolution or GEMM. Right: correct answers faster than eager, split into hybrids (blue) and pure Triton (lime).

Early on, base Qwen naturally adopted this strategy, submitting hybrid implementations 55% of the time on Level 2. In fact, an early checkpoint (gym v5) produced 11 fast hybrid solutions — the best Level 2 score of any Qwen model.

Unfortunately, our later data filtering and gym rules penalized this exact behavior. We filtered out hybrid code during SFT curation and strictly blocked PyTorch compute in the gym. Consequently, the SFT model only submitted hybrids 8% of the time, attempting to reimplement full 2D convolutions from scratch in Triton — code that was both prone to numerical bugs and noticeably slower than cuDNN. In contrast, frontier models like Claude Opus and GPT-6 adopted hybrids in 61% to 93% of their Level 2 answers, accounting for virtually all of their high scores.

6.3 Exercises are not the exam

While staged curriculum exercises (scaffolds and epilogues) unlocked training signals for complex operators, the gains concentrated heavily on the assisted variants. When faced with full, unassisted Level 2 modules under benchmark evaluation, the model struggled to synthesize the complete end-to-end kernel. An effective curriculum must ensure its final stage closely mirrors the unassisted target distribution at full scale.

Eleven Qwen checkpoints on the same 200 problemsCorrect (light) and correct-and-faster-than-eager (dark), share of all 200 problems. Level 1 + level 2, one greedy answer each.0%10%20%30%BaseQwen3.8-27B21.5%7.0%RL from basegym v5 · step 30023.5%11.5%gym v5 · step 50024.5%10.5%gym v6 · step 25021.5%7.0%gym v7 · step 23523.0%8.0%SFTSFT adapter26.0%8.0%SFT + RLgym v8 · step 24129.0%8.5%gym v9 · step 10029.5%9.5%gym v9 · step 15029.0%9.5%gym v9 · step 20027.0%8.5%gym v9 · step 30027.5%8.0%RL from the base model raised fast₁ most (v5 · 300: 11.5%, mostly on level 2); SFT raised correctness most. Step 100 of the v9 run is the shipped adapter.
All eleven Qwen checkpoints we evaluated: RL from the base model, SFT, and SFT followed by RL in two gym versions. SFT and SFT + RL lead on correctness; RL from the base model leads on fast₁ through level 2 hybrids.

7. Frontier models on the same 200 problems

To put these results in perspective, we evaluated four frontier commercial models on the exact same 200 KernelBench problems using the same H100 execution grader. Frontier models were run through their respective CLI interfaces using standard reasoning modes, meaning this is not a controlled parameter-for-parameter match with greedy, non-reasoning Qwen. Nevertheless, the comparison offers valuable architectural insights:

Frontier models on the same 200 problemsSame prompts, same H100 grader. Frontier answers came from vendor CLIs with medium or default reasoning; Qwen answered greedily without thinking.L1 correctL1 fast₁L2 correctL2 fast₁Qwen3.8-27B base318126Qwen3.8-27B SFT + RL4715124Fable 5.164317655Opus 5.565387257GPT-6 Astra71479780GPT-6.1 Sol73449781Counts out of 100 per level. fast₁ excludes answers flagged for speedups above 10× (1 on level 1 and 5–7 on level 2 per frontier model).Generation settings differ between groups; treat the gap as an order of magnitude, not a controlled comparison.
Correct and correct-and-faster-than-eager answers per level, out of 100. Frontier fast₁ excludes answers flagged for implausible speedups.
Levels 1–2, 200 problems. fast₁ counts flagged speedups (above 10×) as the grader does; the unflagged column removes them.
ModelCorrectfast₁ vs eagerfast₁, unflaggedfast₂fast₁ vs torch.compileMean answer tokens
Qwen3.8-27B base21.5%7.0%7.0%3.5%12.5%not recorded
Qwen3.8-27B SFT + RL29.5%9.5%9.5%5.0%16.0%1,109
Fable 5.170.0%46.5%43.0%17.5%43.5%2,906
Opus 5.568.5%51.5%47.5%22.5%41.0%2,535
GPT-6 Astra84.0%66.5%63.5%19.5%50.0%1,871
GPT-6.1 Sol85.0%66.5%62.5%20.0%51.0%1,747
  • Level 1 vs. Level 2 gap: On standalone Level 1 operators, our tuned 27B model is within striking distance of the frontier: top commercial models lead by ~3× on unflagged fast₁ (47 vs 15). On composite Level 2 modules, however, frontier models lead by 20× (81 vs 4) — driven almost exclusively by their ability to generate hybrid cuDNN + Triton solutions.
  • Implausible speedup flags: Commercial models produced 6 to 8 solutions with reported speedups exceeding 10×. While some reflect genuine fusion wins, extreme outliers often trace back to timing artifacts or benchmark loopholes; filtering these flags trims fast₁ scores by 3 to 4 percentage points.
  • Generation budget disparity: Frontier models consumed substantial reasoning budgets, averaging 1,747 to 2,906 completion tokens per task, compared to roughly 1,109 tokens for our direct greedy outputs.

8. What we would change next

  1. Align the gym with the benchmark baseline: Benchmark against eager PyTorch (or a joint eager + compiled objective) so the reward never incentivizes kernels that KernelBench evaluates as slow. The gym now supports this toggle for upcoming training runs.
  2. Embrace the hybrid pattern: Allow library cuBLAS/cuDNN calls for heavy GEMMs and convolutions in Level 2-style tasks, rewarding the fused elementwise tail, and retain these hybrid implementations in the SFT corpus rather than filtering them out.
  3. Anchor the curriculum to target scale: Retain staged scaffolds for early exploration, but ensure final training stages weight unassisted full tasks at target production shapes.
  4. Multi-sample evaluation: Evaluating a single greedy sample per problem introduces noise, making 1–3 problem differences difficult to interpret. Averaging across three seeded passes per task costs more compute but yields clearer separation between checkpoints.

9. Limits

  • Single training seed and greedy sampling: Reported Qwen numbers reflect a single training run evaluated under greedy temperature-0 decoding.
  • Best-of-run checkpoint selection: Step 100 was selected after observing evaluations across all checkpoints, introducing a mild selection bias.
  • Asymmetric frontier conditions: Commercial frontier models utilized vendor CLI wrappers, multi-turn reasoning traces, and stochastic sampling, whereas Qwen ran under single-turn greedy execution without reasoning.
  • Precision and architecture scope: All evaluations were conducted in FP32 on KernelBench levels 1 and 2 on NVIDIA H100 SXM; Level 3 architectures and alternative GPU generations were not evaluated.
  • Static pattern heuristics: Hybrid classification (“keeps a PyTorch op”) was determined via static AST inspection rather than dynamic runtime profiling.

The telemetry and configuration data for every figure and table in this study are available in the JSON download. The model weights and verified training dataset are open-sourced on Hugging Face: LoRA adapter and verified SFT dataset.