Skip to content

Commit 16e3442

Browse files
abgnydnclaude
andauthored
Publish cross-API Acrobot artifact: implementations + equivalence gate + Colab runner (#1)
* Publish the cross-API Acrobot artifact: implementations, equivalence gate, Colab runner The paper's headline cross-API numbers (720x CUDA / 172x JAX / 27x Triton vs per-step PyTorch on T4) had no published code. This adds it: - benchmarks/colab/acrobot_impls.py — one canonical dynamics definition (Gym "book" Acrobot, explicit Euler dt=0.05, matching the WebGPU shader lineage) with five implementations: NumPy reference, per-step PyTorch baseline, JAX lax.scan, hand-fused CUDA (CuPy RawKernel), Triton - tests/acrobot_equivalence_test.py + CI job — NumPy/PyTorch/JAX must produce IDENTICAL fitness for shared genomes in float64 (verified: 1.0000 match) before any timing means anything - benchmarks/colab/acrobot_cross_api.ipynb — Colab notebook (GPU runtime) that clones the repo, verifies GPU kernels against the same reference (>=95% match required; chaotic-system caveat documented), then times all implementations and saves a results JSON - Label correction: the integrator is explicit Euler, not RK4 as earlier table captions claimed; artifact note added to paper.tex including the framework-versions-are-moving-targets caveat (cross-ref to the companion transformer paper's v2 erratum) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add T4 re-measurement (Colab, 2026-07-28) with verified-equivalent kernels Ran the notebook on a Colab Tesla T4 (python 3.12.13, torch 2.11.0+cu128, jax 0.7.2, cupy 14.0.1). Equivalence vs the NumPy reference: torch 99.66%, fused CUDA 99.63%, jax 99.63% (chaotic f32 flips; gate >=95%). Timings (N=10 medians, POP=4096, 500 steps): per-step PyTorch 554.7ms (1.8 gen/s), fused CUDA 3.15ms (317.3 gen/s, 176x), JAX lax.scan 16.5ms (60.5 gen/s, 33.5x). v1's 720x/172x reflected a ~3x-slower torch — qualitative result unchanged, ratios compressed. Also fixes the Triton kernel for newer Triton (tl.math.tanh removed; manual tanh). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d1fa87b commit 16e3442

8 files changed

Lines changed: 706 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,20 @@ jobs:
2828
# copy-paste errors and stale numbers without needing GPU access.
2929
- name: Verify paper arithmetic
3030
run: node tests/paper_arithmetic.test.js
31+
32+
acrobot-equivalence:
33+
name: acrobot cross-API equivalence (CPU, f64)
34+
runs-on: ubuntu-latest
35+
steps:
36+
- uses: actions/checkout@v4
37+
- uses: actions/setup-python@v5
38+
with:
39+
python-version: "3.12"
40+
cache: pip
41+
- run: pip install numpy jax torch --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple
42+
# NumPy, PyTorch, and JAX must produce IDENTICAL fitness for shared
43+
# genomes — the property that makes the cross-API speedup table
44+
# meaningful. GPU kernels (CUDA/Triton) are checked in the Colab
45+
# notebook against the same reference.
46+
- name: Acrobot implementation equivalence
47+
run: JAX_ENABLE_X64=1 python tests/acrobot_equivalence_test.py

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and the project follows [Semantic Versioning](https://semver.org/) starting
66
from `0.1.0`.
77

8+
## [0.2.0] — 2026-07-28
9+
10+
### Added
11+
12+
- **Cross-API Acrobot artifact published** — the code behind the headline
13+
T4 numbers now lives in `benchmarks/colab/acrobot_impls.py` (NumPy
14+
reference, per-step PyTorch baseline, JAX `lax.scan`, hand-fused CUDA via
15+
CuPy, Triton) with a CI-gated float64 equivalence test and a Colab
16+
notebook runner.
17+
- **Re-measured on Colab T4 (2026-07-28)** with verified-equivalent
18+
kernels: fused CUDA 176x, JAX 33.5x over per-step PyTorch (torch
19+
2.11.0+cu128). v1's 720x/172x reflected a ~3x-slower torch of spring
20+
2026 — ratios compress as frameworks improve; qualitative result
21+
unchanged. Data in `benchmarks/results/2026-07-28_t4_colab/`.
22+
23+
### Fixed
24+
25+
- Acrobot tables were labeled "RK4"; the integrator is explicit Euler at
26+
dt=0.05 (corrected in README, PAPER.md, paper.tex + artifact note).
27+
828
## [0.1.0] — 2026-05-04
929

1030
First public release of the kernel-fusion benchmark suite + paper companion.

PAPER.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ WebGPU executes the **entire 1,500-step simulation as a single compute shader di
117117

118118
### 4.3 Sequential Workload: Acrobot-v1
119119

120-
**Table 3: Acrobot-v1 (POP=4,096, 163 params, 500 steps, RK4)**
120+
**Table 3: Acrobot-v1 (POP=4,096, 163 params, 500 steps, explicit Euler — earlier versions mislabeled this RK4)**
121121

122122
*Same hardware: Tesla T4 (CUDA)*
123123

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
55
[![Live](https://img.shields.io/badge/live-kernelfusion.dev-6ea8ff)](https://kernelfusion.dev)
66
[![Paper arithmetic](https://img.shields.io/badge/paper%20arithmetic-54%20%E2%9C%93-82c98b)](./tests/paper_arithmetic.test.js)
7+
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abgnydn/webgpu-kernel-fusion/blob/main/benchmarks/colab/acrobot_cross_api.ipynb)
78

89
Fusing sequential fitness evaluations into single GPU compute shader dispatches eliminates per-step kernel launch overhead. We prove this across **4 GPU APIs on 2 hardware platforms** — the fusion advantage is **GPU-API-agnostic**.
910

1011
## Key Results
1112

12-
**Same hardware: Tesla T4 (Acrobot-v1, 500 steps, RK4)**
13+
**Same hardware: Tesla T4 (Acrobot-v1, 500 steps, explicit Euler dt=0.05 — earlier versions mislabeled this RK4)**
1314

1415
| System | gen/s | vs PyTorch |
1516
|---|---|---|
@@ -52,6 +53,31 @@ The fusion advantage scales with dispatch overhead fraction. On compute-bound N-
5253
- **Paper (LaTeX):** [paper.tex](paper.tex)
5354
- **DOI:** [10.5281/zenodo.19342888](https://doi.org/10.5281/zenodo.19342888)
5455

56+
### Re-measured (Tesla T4, Colab, 2026-07-28 — corrected artifact, verified-equivalent kernels)
57+
58+
| System | gen/s | vs PyTorch |
59+
|---|---|---|
60+
| PyTorch CUDA per-step (torch 2.11.0) | 1.8 | 1x |
61+
| JAX lax.scan+vmap (jax 0.7.2) | 60.5 | **33.5x** |
62+
| **Hand-fused CUDA kernel (cupy 14.0.1)** | **317.3** | **176x** |
63+
64+
All kernels cross-checked for output equivalence before timing (≥ 99.6% match,
65+
f32 chaotic-flip residue documented). PyTorch's per-step dispatch got ~3x
66+
faster between the v1 measurement and torch 2.11, compressing the ratios —
67+
the qualitative result is unchanged. Raw data:
68+
[`benchmarks/results/2026-07-28_t4_colab/`](benchmarks/results/2026-07-28_t4_colab/).
69+
70+
## Reproduce the Cross-API Table (Colab)
71+
72+
The Acrobot cross-API implementations (per-step PyTorch, hand-fused CUDA,
73+
JAX `lax.scan`, Triton) live in [`benchmarks/colab/acrobot_impls.py`](benchmarks/colab/acrobot_impls.py)
74+
with one canonical dynamics definition. Open
75+
[`benchmarks/colab/acrobot_cross_api.ipynb`](benchmarks/colab/acrobot_cross_api.ipynb)
76+
in Colab on a GPU runtime — it **verifies all implementations produce identical
77+
fitness on shared genomes before any timing** (CI additionally gates NumPy /
78+
PyTorch / JAX equivalence in float64 on every push). Speedups are tied to the
79+
framework versions the notebook prints; treat baselines as moving targets.
80+
5581
## Reproduce Every Result
5682

5783
### Prerequisites
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Acrobot Cross-API Fusion Benchmark (Colab, NVIDIA GPU)\n",
8+
"\n",
9+
"Reproduces the cross-API table of *Single-Kernel Fusion for Sequential Fitness\n",
10+
"Evaluation via WebGPU Compute Shaders* (doi:10.5281/zenodo.19331833): per-step\n",
11+
"PyTorch dispatch vs hand-fused CUDA vs JAX `lax.scan` vs Triton, all evaluating\n",
12+
"the **same function** (verified below before any timing).\n",
13+
"\n",
14+
"**Runtime \u2192 Change runtime type \u2192 GPU** (T4 matches the paper).\n",
15+
"\n",
16+
"All implementations live in the repo (`benchmarks/colab/acrobot_impls.py`) and are\n",
17+
"CI-tested for CPU equivalence; this notebook adds the GPU implementations and timings.\n"
18+
]
19+
},
20+
{
21+
"cell_type": "code",
22+
"execution_count": null,
23+
"metadata": {},
24+
"outputs": [],
25+
"source": [
26+
"!nvidia-smi -L\n",
27+
"!git clone -q https://github.com/abgnydn/webgpu-kernel-fusion.git repo 2>/dev/null || (cd repo && git pull -q)\n",
28+
"import sys; sys.path.insert(0, 'repo/benchmarks/colab')\n"
29+
]
30+
},
31+
{
32+
"cell_type": "code",
33+
"execution_count": null,
34+
"metadata": {},
35+
"outputs": [],
36+
"source": [
37+
"%pip install -q cupy-cuda12x 'jax[cuda12]'\n"
38+
]
39+
},
40+
{
41+
"cell_type": "code",
42+
"execution_count": null,
43+
"metadata": {},
44+
"outputs": [],
45+
"source": [
46+
"import numpy as np, time, json, torch\n",
47+
"import acrobot_impls as A\n",
48+
"print(f'POP={A.POP} GENOME={A.GENOME_SIZE} STEPS={A.STEPS} (explicit Euler dt={A.DT})')\n",
49+
"genomes = A.make_genomes() # shared across every implementation\n"
50+
]
51+
},
52+
{
53+
"cell_type": "markdown",
54+
"metadata": {},
55+
"source": [
56+
"## 1 \u2014 Equivalence before timing\n",
57+
"\n",
58+
"Speedups over kernels that compute different functions are meaningless, so every\n",
59+
"implementation is checked against the NumPy reference on the shared genomes first.\n",
60+
"The double pendulum is chaotic and the argmax policy discontinuous, so f32\n",
61+
"op-order differences can flip a small fraction of episodes \u2014 f64 CPU equivalence\n",
62+
"is exact (CI-gated); for f32 GPU kernels we report the match fraction and require\n",
63+
"\u2265 95%.\n"
64+
]
65+
},
66+
{
67+
"cell_type": "code",
68+
"execution_count": null,
69+
"metadata": {},
70+
"outputs": [],
71+
"source": [
72+
"ref32 = A.acrobot_numpy(genomes)\n",
73+
"\n",
74+
"checks = {}\n",
75+
"checks['torch-cuda f32'] = A.acrobot_torch(genomes, device='cuda')\n",
76+
"checks['fused CUDA'] = A.acrobot_cuda(genomes)\n",
77+
"import jax.numpy as jnp\n",
78+
"jax_eval = A.make_acrobot_jax()\n",
79+
"checks['jax f32'] = np.asarray(jax_eval(jnp.asarray(genomes)))\n",
80+
"try:\n",
81+
" triton_eval = A.make_acrobot_triton()\n",
82+
" checks['triton'] = triton_eval(genomes)\n",
83+
"except Exception as e:\n",
84+
" triton_eval = None\n",
85+
" print(f'Triton unavailable/failed to compile: {e}')\n",
86+
"\n",
87+
"for name, r in checks.items():\n",
88+
" frac = A.match_fraction(ref32, r, atol=1e-4)\n",
89+
" status = 'OK' if frac >= 0.95 else 'FAIL'\n",
90+
" print(f'{name:16} match={frac:.4f} {status}')\n",
91+
" assert frac >= 0.95, f'{name} diverged \u2014 investigate before timing'\n"
92+
]
93+
},
94+
{
95+
"cell_type": "markdown",
96+
"metadata": {},
97+
"source": [
98+
"## 2 \u2014 Timings (N=10, medians, gen/s = full-population evaluations per second)\n"
99+
]
100+
},
101+
{
102+
"cell_type": "code",
103+
"execution_count": null,
104+
"metadata": {},
105+
"outputs": [],
106+
"source": [
107+
"N = 10\n",
108+
"def bench(fn, warmup=3):\n",
109+
" for _ in range(warmup): fn()\n",
110+
" ts = []\n",
111+
" for _ in range(N):\n",
112+
" t0 = time.perf_counter(); fn(); ts.append(time.perf_counter() - t0)\n",
113+
" ts.sort()\n",
114+
" med = ts[len(ts)//2]\n",
115+
" return med\n",
116+
"\n",
117+
"g_cuda = torch.as_tensor(genomes, device='cuda')\n",
118+
"results = {}\n",
119+
"results['pytorch per-step (baseline)'] = bench(lambda: A.acrobot_torch(genomes, device='cuda'))\n",
120+
"results['fused CUDA (cupy)'] = bench(lambda: A.acrobot_cuda(genomes))\n",
121+
"gj = jnp.asarray(genomes)\n",
122+
"results['jax lax.scan (jit)'] = bench(lambda: np.asarray(jax_eval(gj)))\n",
123+
"if triton_eval: results['triton fused'] = bench(lambda: triton_eval(genomes))\n"
124+
]
125+
},
126+
{
127+
"cell_type": "code",
128+
"execution_count": null,
129+
"metadata": {},
130+
"outputs": [],
131+
"source": [
132+
"base = results['pytorch per-step (baseline)']\n",
133+
"print(f\"{'implementation':30} {'median':>10} {'gen/s':>8} {'speedup':>9}\")\n",
134+
"for name, med in results.items():\n",
135+
" print(f'{name:30} {med*1000:8.1f}ms {1/med:8.1f} {base/med:8.1f}x')\n",
136+
"\n",
137+
"out = {'gpu': torch.cuda.get_device_name(0), 'n': N,\n",
138+
" 'pop': A.POP, 'steps': A.STEPS,\n",
139+
" 'results_ms': {k: round(v*1000, 2) for k, v in results.items()},\n",
140+
" 'speedups': {k: round(base/v, 1) for k, v in results.items()}}\n",
141+
"print(json.dumps(out, indent=2))\n",
142+
"with open('acrobot_cross_api_results.json', 'w') as f: json.dump(out, f, indent=2)\n",
143+
"print('\\nSaved acrobot_cross_api_results.json \u2014 commit it under benchmarks/results/.')\n"
144+
]
145+
},
146+
{
147+
"cell_type": "markdown",
148+
"metadata": {},
149+
"source": [
150+
"## Notes\n",
151+
"\n",
152+
"- The baseline is *per-step framework dispatch* \u2014 the pattern naive GPU RL/EC\n",
153+
" evaluation uses. It is a fair representation of that pattern, not of the best\n",
154+
" possible unfused pipeline (e.g. CUDA graphs would batch launches).\n",
155+
"- Speedups are tied to the framework versions printed by the install cell \u2014\n",
156+
" they are moving targets; report versions with any numbers you publish.\n",
157+
"- Dynamics: Gym's 'book' Acrobot equations, explicit Euler at dt=0.05 across\n",
158+
" ALL implementations (earlier artifacts mislabeled this as RK4).\n"
159+
]
160+
}
161+
],
162+
"metadata": {
163+
"accelerator": "GPU",
164+
"colab": {
165+
"gpuType": "T4",
166+
"provenance": []
167+
},
168+
"kernelspec": {
169+
"display_name": "Python 3",
170+
"name": "python3"
171+
},
172+
"language_info": {
173+
"name": "python"
174+
}
175+
},
176+
"nbformat": 4,
177+
"nbformat_minor": 0
178+
}

0 commit comments

Comments
 (0)