PopcornBench is an extension of KernelBench with a multi-model agentic sweep harness for evaluating how well LLMs can write fast, numerically equivalent CUDA / Triton / Tilelang kernels against a PyTorch reference. Each run pairs a model with a level and problem set, drives a tool-using agent loop with compile / correctness / submit tools, and records per-problem speedups, fusion ratios, energy, and roofline metrics.
Plan
We score each correct kernel on three signals and compare submissions on the Pareto frontier across all three:
- Runtime. Wall-clock per call relative to the PyTorch reference. Reported as speedup = ref_runtime / runtime.
- SOL. max(dram_utilization, compute_utilization) / 100 from Nsight counters, bounded in [0, 1]. Heuristic fallback when ncu is unavailable.
- Energy. Joules per call from NVML (energy_per_run_mj). Meaningful only when per-call runtime exceeds the NVML sampling window (~10 ms), so reported on L3 and L4 only.
A kernel is considered to dominate another only if it is at least as good on all three signals and strictly better on one. Models are then ranked by the size of their Pareto-optimal subset within each (level, variant).
Eval architecture
The harness decouples agent oversubscription from GPU contention via a per-GPU FIFO eval queue. Each GPU runs one dedicated eval-server process that drains a Manager-backed queue in arrival order; agents push submit_kernel requests and block on the response while their next-turn LLM call is free to run in parallel for other problems. With agents_per_gpu at 4-6 (vs. the previous 1-3) the GPU stays busy without locks starving any agent past worker_timeout_s.
Multi-GPU communication kernels (NCCL collectives, real tensor parallelism, pipeline parallelism — six problems in L2 popcorn) need all GPUs visible to one worker, which is incompatible with per-GPU pinning, so they are skipped from the broad sweeps and run separately via sweep.comm.toml.
Sweep matrix
Two variants (original, popcorn), four levels (L1-L4), three tool tiers (single = no tools, default, all). Each sweep covers both variants. We split L1+L2 from L3+L4 because the heavy levels need fewer perf trials and lower agent oversubscription to stay inside worker timeouts. Plus a separate comm sweep for the multi-GPU L2 popcorn problems and two AlphaEvolve focus runs.
| Run | Coverage | Tools | Models |
|---|---|---|---|
l12_single | L1+L2, original + popcorn (excl. multi-GPU) | submit_kernel only, max_turns=1 | gpt-5.5-priority, Kimi-K2.6 |
l12_default | same coverage | default (compile, correctness, specs, static_check, submit) | same two |
l12_all | same coverage | all (default + profile, disassemble, ert) | same two |
l34_single | L3+L4, original + popcorn | submit_kernel only, max_turns=1 | same two |
l34_default | same coverage | default | same two |
l34_all | same coverage | all | same two |
comm_l2_popcorn | L2 popcorn multi-GPU subset (problems 2, 11, 18, 27, 34, 38) | default | same two |
ae_focus_popcorn | L1-L4 popcorn, problems 1, 2, 10 | AE evolutionary search | AlphaEvolve (Gemini 3.0 mixture) |
ae_focus_original | L1-L2 original, problems 1, 2, 10 | AE evolutionary search | AlphaEvolve (Gemini 3.0 mixture) |
Run order
Each sweep is GPU-bound and contests the full 8xH100 box, so the primary sweeps run sequentially. The aeproxy server and the gh-pages publisher run as background daemons throughout. Each step runs in a detached tmux session so SSH disconnects don't kill it.
| Step | tmux session | Command | Wall-clock | Parallel with |
|---|---|---|---|---|
| 0a (background) | aeproxy | tmux new -d -s aeproxy 'cd /scratch/tejas/sp26-ae-llm && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python -m aeproxy.server' | persistent | everything (idle until step 4) |
| 0b (background) | publish | tmux new -d -s publish 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/publish_to_gh_pages.py --watch 300' | persistent | everything |
| 1 | smoke | tmux new -d -s smoke 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.smoke_all.toml' | ~15 min | nothing (alone) |
| 2 | l12_single | tmux new -d -s l12_single 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.l12_single.toml' | ~30 min | nothing (alone) |
| 3 | l12_default | tmux new -d -s l12_default 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.l12_default.toml' | ~3-4 hours | nothing (alone) |
| 4 | l12_all | tmux new -d -s l12_all 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.l12_all.toml' | ~6-8 hours | nothing (alone) |
| 5 | l34_single | tmux new -d -s l34_single 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.l34_single.toml' | ~2-3 hours | nothing (alone) |
| 6 | l34_default | tmux new -d -s l34_default 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.l34_default.toml' | ~12-15 hours | nothing (alone) |
| 7 | l34_all | tmux new -d -s l34_all 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.l34_all.toml' | ~18-24 hours | nothing (alone) |
| 8 | comm | tmux new -d -s comm 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.comm.toml' | ~3-6 hours | nothing (alone, needs all GPUs) |
| 9 | aepop | tmux new -d -s aepop 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.ae_focus.toml' | ~3-4 hours | nothing (alone) |
| 10 | aeorig | tmux new -d -s aeorig 'cd /scratch/tejas/PopcornBench && export LD_LIBRARY_PATH=/opt/conda/lib:$LD_LIBRARY_PATH; uv run python scripts/run_sweep.py configs/sweep.ae_focus_orig.toml' | ~1.5-2 hours | nothing (alone) |
Total wall-clock for all 10 steps is roughly 50-65 hours. If time is tight, the priority order to drop is: aeorig (step 10), l34_all (step 7, the heaviest cell), l12_all (step 4), comm (step 8). Single-turn baselines and default-tier sweeps are most important since they ground the comparison.
Planned figures
- Pareto frontier in (speedup, SOL). One panel per level. Source:
l12_default+l34_default. - Pareto frontier in (speedup, 1 / energy_ratio). L3 and L4 only. Source:
l34_default. - Speedup vs SOL scatter, all problems. One dot per (model, problem), colored by level, faceted by variant. Source:
l12_default+l34_default. - Tier comparison: single vs default vs all. Per (model, level), correctness rate and speedup distribution across the three tool tiers. Source:
l12_*+l34_*. - Correctness rate stacked by tier on L2-popcorn. Source: existing
cuda_l2pop_*sweeps. - SOL distribution by tier on L2-popcorn. Source: existing L2-pop sweeps after the SOL clamp fix.
- AlphaEvolve speedup vs best non-AE speedup, per problem. Source:
ae_focus_popcorn+ae_focus_originaljoined tol12_default/l34_default. - AlphaEvolve original-vs-popcorn paired bars, L1 and L2 only. Source:
ae_focus_popcorn+ae_focus_original, restricted to problems 1, 2, 10. - Failure-mode breakdown per (model, level). Stacked correct / wrong / compile_fail / timeout / error. Source:
l12_default+l34_default. - Communication kernels (separate panel). Per-model speedup on the L2 popcorn comm subset, reported separately because they run in serial multi-GPU mode. Source:
comm_l2_popcorn.