|
| 1 | +# PallasBench on GPU: Motivation, Findings, and Next Steps |
| 2 | + |
| 3 | +## Why JAX |
| 4 | + |
| 5 | +JAX is Google's numerical computing library built on XLA (Accelerated Linear Algebra). Unlike PyTorch, which compiles operator-by-operator, JAX traces entire computation graphs and compiles them as a unit through XLA. This gives JAX three properties that matter for kernel engineering: |
| 6 | + |
| 7 | +- **Functional purity**: every operation is a pure function over immutable arrays, which makes program transformations (autodiff, vectorization, parallelism) composable and predictable. |
| 8 | +- **XLA compilation**: `jax.jit` lowers Python to StableHLO IR, then to device-specific code. On GPU this means CUDA/PTX; on TPU it means TPU HLO. The compiler handles fusion, tiling, and memory placement automatically. |
| 9 | +- **Hardware portability**: the same JAX program runs on CPU, GPU, and TPU without source changes — the backend handles the translation. |
| 10 | + |
| 11 | +For AI workloads, JAX is the training framework behind Gemini, PaLM, and most of Google DeepMind's research. It dominates TPU workloads and has a growing GPU footprint, especially for large-scale distributed training. |
| 12 | + |
| 13 | +## Why Pallas |
| 14 | + |
| 15 | +Pallas is JAX's embedded DSL for writing custom kernels. It sits between "write CUDA by hand" and "hope XLA fuses things correctly": |
| 16 | + |
| 17 | +```python |
| 18 | +from jax.experimental import pallas as pl |
| 19 | + |
| 20 | +def _relu_kernel(x_ref, o_ref): |
| 21 | + x = x_ref[...] |
| 22 | + o_ref[...] = jnp.maximum(x, 0) |
| 23 | + |
| 24 | +out = pl.pallas_call( |
| 25 | + _relu_kernel, |
| 26 | + out_shape=jax.ShapeDtypeStruct(x.shape, x.dtype), |
| 27 | + grid=(n // block_size,), |
| 28 | + in_specs=[pl.BlockSpec((block_size, cols), lambda i: (i, 0))], |
| 29 | + out_specs=pl.BlockSpec((block_size, cols), lambda i: (i, 0)), |
| 30 | +)(x) |
| 31 | +``` |
| 32 | + |
| 33 | +You write Python that looks like NumPy, but you explicitly control: |
| 34 | +- **Grid**: how many blocks to launch |
| 35 | +- **BlockSpec**: what tile each block reads/writes |
| 36 | +- **Memory refs**: explicit load/store through `Ref` objects |
| 37 | + |
| 38 | +Pallas then compiles this to: |
| 39 | +- **Triton IR** on NVIDIA GPUs (via the Triton compiler) |
| 40 | +- **Mosaic** on Google TPUs (via the TPU compiler) |
| 41 | + |
| 42 | +This is powerful because the same Pallas kernel can target both TPU and GPU — the abstraction handles the backend translation. No other framework offers this. |
| 43 | + |
| 44 | +### Why not just write Triton directly? |
| 45 | + |
| 46 | +Triton is NVIDIA-only. Pallas is portable. If you're building kernels for a system that might run on TPU (training) and GPU (inference), Pallas lets you write once. It also integrates natively with JAX's transformation system — you get `jax.grad`, `jax.vmap`, and `jax.jit` composition for free. |
| 47 | + |
| 48 | +### Why not just let XLA auto-fuse? |
| 49 | + |
| 50 | +XLA is good at fusing simple patterns (elementwise chains, reduce-broadcast), but it cannot discover complex fusion patterns like flash attention, fused SwiGLU, or tiled matrix-multiply-accumulate. For these, you need explicit tiling control — that's what Pallas provides. |
| 51 | + |
| 52 | +## What is PallasBench |
| 53 | + |
| 54 | +[PallasBench](https://github.com/Tyronita/PallasBench) is a benchmark suite of 45 Pallas kernels across three difficulty levels: |
| 55 | + |
| 56 | +| Level | Count | Description | Examples | |
| 57 | +|-------|-------|-------------|----------| |
| 58 | +| L1 | 27 | Single operators | relu, softmax, matmul, layernorm, reduce_sum | |
| 59 | +| L2 | 13 | Fused patterns | matmul+gelu, SwiGLU, fused softmax cross-entropy | |
| 60 | +| L3 | 5 | Architecture components | flash attention, multi-head attention, transformer block | |
| 61 | + |
| 62 | +Each kernel comes with: |
| 63 | +- A **Pallas implementation** (the kernel under test) |
| 64 | +- A **JAX baseline** (reference implementation using standard `jnp` ops) |
| 65 | +- **Input shapes** and evaluation harness for correctness + timing |
| 66 | + |
| 67 | +### Is it built for a specific use case? |
| 68 | + |
| 69 | +PallasBench was originally designed for **TPU evaluation**. The kernels use Pallas primitives that map naturally to TPU's systolic array architecture. GPU support was added later (the repo's single commit message: *"Fix Pallas GPU lowering by using Triton backend"*), but the kernels were **not tuned for GPU**. |
| 70 | + |
| 71 | +This means: |
| 72 | +- Block sizes don't account for GPU SM count (108 on A100), warp size (32), or shared memory (164KB/SM) |
| 73 | +- Tiling doesn't target GPU memory hierarchy (L2 cache: 40MB, HBM bandwidth: 2039 GB/s) |
| 74 | +- No use of GPU-specific Triton features (tl.dot, async loads, persistent kernels) |
| 75 | + |
| 76 | +This is actually what makes PallasBench interesting for our work: it's a clean set of **unoptimized** kernels that represent what an LLM would generate when asked to write Pallas code without GPU-specific tuning knowledge. |
| 77 | + |
| 78 | +## What We Found: GPU Compilation Issues |
| 79 | + |
| 80 | +When we ran PallasBench on an NVIDIA A100 80GB, every kernel failed with: |
| 81 | + |
| 82 | +``` |
| 83 | +INVALID_ARGUMENT: Maximum allowed number of elements is 1048576, |
| 84 | +but tensor<1024x4096xi32> has more than that |
| 85 | +``` |
| 86 | + |
| 87 | +The Triton backend enforces a **1M element limit per tensor operation**. PallasBench kernels use `block_size = min(1024, n)` with the full second dimension in the block, giving blocks like `(1024, 4096)` = 4M elements. |
| 88 | + |
| 89 | +### Our Fix |
| 90 | + |
| 91 | +We patched all 35 affected kernel files to clamp block sizes to respect the Triton limit: |
| 92 | + |
| 93 | +```python |
| 94 | +# Before (fails on GPU): |
| 95 | +block_size = min(1024, n) |
| 96 | + |
| 97 | +# After (GPU-compatible): |
| 98 | +cols = 1 |
| 99 | +for s in x.shape[1:]: |
| 100 | + cols *= s |
| 101 | +block_size = min(min(1024, n), max(1, 1048576 // cols)) |
| 102 | +``` |
| 103 | + |
| 104 | +For `(4096, 4096)` inputs, this gives `block_size = 256`, and blocks of `(256, 4096)` = 1,048,576 elements — exactly at the Triton limit. |
| 105 | + |
| 106 | +The full patch modifies 35 files with +106/-12 lines. Every change is mechanical: compute the column product, clamp the row block size. The fix preserves correctness because Pallas's `BlockSpec` handles the tiling — we're just choosing smaller tiles. |
| 107 | + |
| 108 | +## What We're Capturing |
| 109 | + |
| 110 | +For every kernel, we collect: |
| 111 | + |
| 112 | +| Artifact | Description | |
| 113 | +|----------|-------------| |
| 114 | +| `original.py` | Upstream PallasBench source (pre-fix) | |
| 115 | +| `fixed.py` | Our GPU-compatible version | |
| 116 | +| `fix.diff` | Unified diff showing exact changes | |
| 117 | +| `jaxpr.txt` | JAX's functional IR — the compute DAG with grid/block metadata | |
| 118 | +| `stablehlo.txt` | StableHLO IR with embedded Triton MLIR bytecode | |
| 119 | +| `result.json` | Correctness, baseline vs kernel timing, speedup, throughput | |
| 120 | +| `stdout.log` | Full compilation and runtime output | |
| 121 | +| `stderr.log` | XLA/Triton compiler diagnostics | |
| 122 | +| GPU snapshots | Memory usage, utilization, temperature, power before/after | |
| 123 | + |
| 124 | +### Metrics per kernel |
| 125 | + |
| 126 | +- **Correctness**: pass/fail across 3 random seeds, max absolute error |
| 127 | +- **Baseline time (ms)**: standard JAX `jnp` implementation |
| 128 | +- **Kernel time (ms)**: Pallas kernel on GPU via Triton |
| 129 | +- **Speedup**: baseline / kernel time |
| 130 | +- **Throughput (GB/s)**: bytes read + written / kernel time |
| 131 | +- **HW bandwidth utilization (%)**: throughput / A100 peak (2039 GB/s) |
| 132 | +- **GPU memory delta (MB)**: HBM allocated by the kernel |
| 133 | +- **JIT compilation time**: wall-clock time including Triton compile |
| 134 | + |
| 135 | +## What We Want to Expand To |
| 136 | + |
| 137 | +### 1. Multi-backend comparison |
| 138 | + |
| 139 | +Run the same PallasBench kernels on: |
| 140 | +- **GPU via Triton** (current — A100) |
| 141 | +- **GPU via Mosaic GPU** (JAX's newer CUDA backend, when available) |
| 142 | +- **TPU** (the original target — on Google Cloud TPU v4/v5) |
| 143 | +- **CPU** (XLA CPU backend, for baseline) |
| 144 | + |
| 145 | +This gives a cross-platform kernel performance matrix that doesn't exist anywhere else. |
| 146 | + |
| 147 | +### 2. Multi-size scaling |
| 148 | + |
| 149 | +Run each kernel at multiple input sizes to characterize: |
| 150 | +- Compute-bound vs memory-bound crossover point |
| 151 | +- Tiling efficiency at different scales |
| 152 | +- Kernel launch overhead vs computation |
| 153 | + |
| 154 | +Target sizes: `(256,256)`, `(1024,1024)`, `(4096,4096)`, `(8192,8192)` |
| 155 | + |
| 156 | +### 3. NCU profiling integration |
| 157 | + |
| 158 | +Use NVIDIA Nsight Compute to capture per-kernel: |
| 159 | +- SM occupancy |
| 160 | +- L2 cache hit rate |
| 161 | +- Warp execution efficiency |
| 162 | +- Arithmetic intensity (FLOP/byte) |
| 163 | +- Memory throughput breakdown (L1/L2/HBM) |
| 164 | + |
| 165 | +### 4. Kernel optimization as training data |
| 166 | + |
| 167 | +The PallasBench kernels are deliberately unoptimized. We want to: |
| 168 | +1. Use LLMs to generate **optimized versions** of each kernel |
| 169 | +2. Evaluate them with the same harness (correctness + speedup) |
| 170 | +3. Build a dataset of `(unoptimized kernel, optimized kernel, speedup)` triples |
| 171 | +4. Use this as SFT/RL training data for code-generation models |
| 172 | + |
| 173 | +This fits the **KernelBench** paradigm: given a reference implementation, produce a faster kernel. But for Pallas instead of CUDA/Triton. |
| 174 | + |
| 175 | +### 5. Integration with ShinkaEvolve |
| 176 | + |
| 177 | +Our evolutionary optimization framework ([ShinkaEvolve](https://github.com/Tyronita/ShinkaEvolve)) already runs on CVDP (Verilog design) and KernelBench (CUDA). Adding PallasBench as a target means: |
| 178 | +- Evolve Pallas kernels with correctness + speedup as the fitness function |
| 179 | +- Capture full evolutionary traces (generations, patches, reward signals) |
| 180 | +- Build the same KernelBook-format dataset we produce for CVDP and Verilog |
| 181 | + |
| 182 | +### 6. Cross-benchmark dataset unification |
| 183 | + |
| 184 | +We're building a unified kernel benchmark dataset across: |
| 185 | + |
| 186 | +| Benchmark | Language | Tasks | Status | |
| 187 | +|-----------|----------|-------|--------| |
| 188 | +| KernelBench | CUDA/Triton | 250 | Downloaded | |
| 189 | +| PallasBench | Pallas/JAX | 45 | Running eval | |
| 190 | +| CVDP | SystemVerilog | 304 | Uploaded to HF | |
| 191 | +| Verilog Eval | Verilog | 157 | Uploaded to HF | |
| 192 | +| MultiKernelBench | CUDA/Triton/Pallas/AscendC | 285 | Cloned | |
| 193 | +| KernelBench-v2 | Triton | ~250 | Cloned | |
| 194 | +| KernelBot | CUDA (competition) | varies | Downloaded | |
| 195 | + |
| 196 | +All formatted as KernelBook-compatible JSONL with: |
| 197 | +- Task description and reference implementation |
| 198 | +- Generated kernel source |
| 199 | +- Correctness signal (pass/fail) |
| 200 | +- Performance signal (speedup, throughput, utilization) |
| 201 | +- Hardware context (GPU model, memory, clocks) |
| 202 | +- IR representations (Jaxpr, StableHLO, Triton MLIR where applicable) |
| 203 | + |
| 204 | +## Hardware Context |
| 205 | + |
| 206 | +All GPU results are from: |
| 207 | +- **NVIDIA A100 80GB PCIe** (Azure Standard_NC24ads_A100_v4) |
| 208 | +- 108 SMs, 6912 CUDA cores, 432 Tensor cores |
| 209 | +- 80GB HBM2e, 2039 GB/s bandwidth |
| 210 | +- 40MB L2 cache |
| 211 | +- 19.5 TFLOPS FP32, 312 TFLOPS FP16 Tensor |
| 212 | +- Driver: latest Azure-managed |
| 213 | +- JAX 0.10.1, Triton 3.7.0 |
| 214 | + |
| 215 | +## Conclusion |
| 216 | + |
| 217 | +PallasBench fills a gap: there are many CUDA and Triton kernel benchmarks, but no systematic Pallas benchmark with GPU results, IR captures, and optimization traces. By fixing the GPU compilation issues, capturing comprehensive metrics, and formatting the results as training data, we're building the dataset needed to train LLMs that can write and optimize Pallas kernels — the only kernel DSL that targets both GPU and TPU from a single source. |
| 218 | + |
| 219 | +--- |
| 220 | + |
| 221 | +*Generated 2026-05-30. Dataset and code at [github.com/Tyronita/PallasBench](https://github.com/Tyronita/PallasBench).* |
0 commit comments