Model performance (probjax.nn)¶
End-to-end forward and train-step cost of probjax.nn models and of the scan_layers / remat_policy options, measured on the RTX 3090 (gpu_nn.json, kernel_nn_gpu.json) and on CPU (kernel_nn_cpu.json). Kernel-level numbers are on the attention and kernel pages, setup on the overview. A train step is value_and_grad of a scalar loss plus an optimizer update. Times are medians in milliseconds. OOM: out of device memory. fail†: XLA found no kernel configuration that fits during autotuning (NOT_FOUND), so the case did not run. unsup.: the implementation rejects the shape or backend. error: another failure (the JSON row has the message). —: not measured.
Transformer and DiffusionTransformer¶
The standard profile (model width 256, 8 heads of 32, 4 layers), batch 8, with each attention_fn. The default dot_product_attention dispatches to flex_attention on NVIDIA GPUs (rules).
Batch 8, width 256, 4 layers, RTX 3090. cuDNN has no fp32 path.
At 4096 tokens in bf16 the default train step takes 50.1 ms against 87.5 ms with cuDNN attention; jax.nn XLA attention is 225 at that size. At this head size (32) the probjax kernel is faster than cuDNN in both the forward and the train step.
Table
| model | dtype | tokens | op | default dot_product_attention |
flex_attention |
jax.nn XLA |
jax.nn cuDNN |
|---|---|---|---|---|---|---|---|
| transformer | bf16 | 256 | forward | 0.533 | 0.507 | 0.748 | 0.582 |
| transformer | bf16 | 256 | train step | 2.70 | 2.42 | 3.28 | 2.66 |
| transformer | bf16 | 1024 | forward | 2.20 | 2.19 | 5.58 | 2.95 |
| transformer | bf16 | 1024 | train step | 7.15 | 7.19 | 17.7 | 9.72 |
| transformer | bf16 | 4096 | forward | 14.8 | 14.8 | 66.6 | 22.5 |
| transformer | bf16 | 4096 | train step | 50.1 | 50.3 | 225 | 87.5 |
| transformer | fp32 | 256 | forward | 0.875 | 0.914 | 1.18 | — |
| transformer | fp32 | 256 | train step | 3.51 | 3.45 | 4.31 | — |
| transformer | fp32 | 1024 | forward | 4.18 | 4.17 | 8.19 | — |
| transformer | fp32 | 1024 | train step | 15.3 | 15.2 | 24.4 | — |
| transformer | fp32 | 4096 | forward | 28.2 | 28.8 | 92.3 | — |
| transformer | fp32 | 4096 | train step | 126 | 127 | OOM | — |
| diffusion | bf16 | 256 | forward | 0.628 | 0.632 | 0.847 | 0.629 |
| diffusion | bf16 | 256 | train step | 3.04 | 3.20 | 3.97 | 3.54 |
| diffusion | bf16 | 1024 | forward | 2.35 | 2.31 | 5.69 | 3.02 |
| diffusion | bf16 | 1024 | train step | 8.52 | 8.19 | 19.2 | 11.4 |
| diffusion | bf16 | 4096 | forward | 15.0 | 15.0 | 66.7 | 22.7 |
| diffusion | bf16 | 4096 | train step | 52.4 | 52.6 | 228 | 88.5 |
| diffusion | fp32 | 256 | forward | 0.993 | 0.951 | 1.26 | — |
| diffusion | fp32 | 256 | train step | 4.13 | 4.03 | 4.86 | — |
| diffusion | fp32 | 1024 | forward | 4.25 | 4.25 | 8.28 | — |
| diffusion | fp32 | 1024 | train step | 16.3 | 16.6 | 26.1 | — |
| diffusion | fp32 | 4096 | forward | 28.1 | 28.8 | 92.4 | — |
| diffusion | fp32 | 4096 | train step | 128 | 128 | OOM | — |
Scan over layers and rematerialization¶
Transformer and DiffusionTransformer take remat_policy=None | 'full' | 'nothing_saveable' | <jax.checkpoint_policies name> | callable and scan_layers=False. scan_layers=True stacks the layer parameters and runs the blocks under jax.lax.scan; it changes the parameter tree, so checkpoints of an unscanned model do not load into a scanned one. Both are opt-in.
24 layers, width 512, 4096 tokens, batch 2, bf16, RTX 3090; device peak in a fresh process. The last row uses einsum attention (implementation='xla'), which materializes the scores.
remat_policy='full' cuts the device peak of the 24-layer bf16 model from 5,212 to 2,072 MB for 260 instead of 212 ms per step (the forward runs twice), and is what lets the einsum-attention model train at all at this length. scan_layers=True compiles in 1.07 s instead of 7.32 s, but without remat it keeps every layer's activations as scan residuals and runs out of memory. For deep stacks use scan_layers=True, remat_policy='full'; dots_saveable sits between the two.
Tables (GPU 24 and 12 layers; CPU)
GPU, Transformer, 24 layers, flex_attention
scan_layers |
remat_policy |
train step (ms) | compile (s) | XLA temp (MB) | device peak (MB) |
|---|---|---|---|---|---|
| no | none | 212 | 9.46 | 3,460 | 5,212 |
| no | dots_saveable | 248 | 7.13 | 1,808 | 3,560 |
| no | full | 260 | 7.32 | 320 | 2,072 |
| yes | none | 248 | 1.83 | 7,373 | OOM |
| yes | full | 257 | 1.07 | 235 | 2,126 |
GPU, DiffusionTransformer, 24 layers, flex_attention
scan_layers |
remat_policy |
train step (ms) | compile (s) | XLA temp (MB) | device peak (MB) |
|---|---|---|---|---|---|
| no | none | 229 | 16.9 | 3,848 | 6,894 |
| no | dots_saveable | 263 | 13.0 | 1,824 | 4,870 |
| no | full | 274 | 13.8 | 328 | 3,374 |
| yes | none | 257 | 2.90 | 8,081 | OOM |
| yes | full | 265 | 1.55 | 204 | 3,385 |
GPU, Transformer, 24 layers, einsum attention
scan_layers |
remat_policy |
train step (ms) | compile (s) | XLA temp (MB) | device peak (MB) |
|---|---|---|---|---|---|
| no | none | OOM | — | — | OOM |
| no | dots_saveable | OOM | — | — | OOM |
| no | full | 492 | 8.69 | 2,808 | 4,560 |
| yes | none | OOM | — | — | OOM |
| yes | full | 489 | 1.22 | 2,769 | 4,661 |
GPU, Transformer, 12 layers, einsum attention
scan_layers |
remat_policy |
train step (ms) | compile (s) | XLA temp (MB) | device peak (MB) |
|---|---|---|---|---|---|
| no | none | OOM | — | — | OOM |
| no | dots_saveable | OOM | — | — | OOM |
| no | full | 245 | 4.10 | 2,712 | 3,599 |
| yes | none | OOM | — | — | OOM |
| yes | full | 245 | 1.22 | 2,673 | 3,619 |
CPU, width 512, 512 tokens, batch 2, fp32, 24 layers
scan_layers |
remat_policy |
train step (ms) | compile (s) | XLA temp (MB) | device peak (MB) |
|---|---|---|---|---|---|
| no | none | 1,755 | 3.19 | 2,042 | — |
| no | dots_saveable | 1,541 | 3.06 | 670 | — |
| no | full | 1,505 | 3.07 | 124 | — |
| yes | none | 2,110 | 0.52 | 2,130 | — |
| yes | full | 1,611 | 0.45 | 26.0 | — |
CPU, 12 layers
scan_layers |
remat_policy |
train step (ms) | compile (s) | XLA temp (MB) | device peak (MB) |
|---|---|---|---|---|---|
| no | none | 881 | 1.56 | 1,034 | — |
| no | dots_saveable | 785 | 1.42 | 382 | — |
| no | full | 761 | 1.40 | 124 | — |
| yes | none | 1,035 | 0.49 | 1,101 | — |
| yes | full | 811 | 0.44 | 40.0 | — |
On CPU (24 layers, 512 tokens), remat_policy='full' is also faster, not only smaller: 1,505 vs 1,755 ms per step, because the smaller working set stays in cache.
SSMModel cells¶
SSMModel(16, 128, 16, 4) with each recurrent cell, batch 8, fp32. Mamba2Cell is the full Mamba-2 block on ssd (expand=2, short causal convolution, gated RMSNorm, head_dim=64, state_dim=64); SSDCell is a multi-head SSD layer with the Mamba-2 decay parameterization; MambaCell uses state_dim = model_dim by default; GatedDeltaNetCell and LRUCell use their defaults.
SSMModel(16, 128, 16, 4), batch 8, fp32, RTX 3090.
| cell | parameters | tokens | forward (ms) | train step (ms) | train-step compile (s) |
|---|---|---|---|---|---|
LRUCell |
995,728 | 1024 | 1.61 | 5.43 | 5.07 |
LRUCell |
995,728 | 4096 | 6.20 | 18.3 | 5.53 |
SSDCell |
863,644 | 1024 | 1.90 | 7.17 | 10.4 |
SSDCell |
863,644 | 4096 | 7.18 | 25.2 | 10.8 |
Mamba2Cell |
1,136,064 | 1024 | 3.12 | 12.1 | 11.3 |
Mamba2Cell |
1,136,064 | 4096 | 11.7 | 44.6 | 10.1 |
GatedDeltaNetCell |
1,006,144 | 1024 | 2.62 | 11.9 | 8.33 |
GatedDeltaNetCell |
1,006,144 | 4096 | 10.2 | 41.1 | 8.49 |
MambaCell |
930,704 | 1024 | 5.09 | 37.2 | 6.38 |
MambaCell |
930,704 | 4096 | 18.7 | 147 | 6.29 |
For the kernels underneath see SSD, Mamba and Gated DeltaNet.
UNet¶
UNet(3, (64, 128, 256)), batch 8, RTX 3090; device peak in a fresh process:
| dtype | resolution | op | time (ms) | XLA temp (MB) | device peak (MB) |
|---|---|---|---|---|---|
| bf16 | 32 | forward | 1.15 | 11.8 | — |
| bf16 | 32 | train step | 3.97 | 72.7 | — |
| bf16 | 64 | forward | 2.91 | 44.8 | 254 |
| bf16 | 64 | train step | 10.1 | 225 | 398 |
| fp32 | 32 | forward | 1.64 | 23.5 | — |
| fp32 | 32 | train step | 5.41 | 114 | — |
| fp32 | 64 | forward | 4.70 | 83.0 | 261 |
| fp32 | 64 | train step | 15.3 | 436 | 12,662 |
bf16 train step at resolution 64 with remat or donated buffers. Remat costs time without reducing temporaries at this size:
| variant | time (ms) | compile (s) | XLA temp (MB) |
|---|---|---|---|
| plain | 9.56 | 14.4 | 225 |
| remat | 13.3 | 2.53 | 231 |
| donate | 9.41 | 2.27 | 238 |
MoE layer¶
Dropless MoELayer, 8 experts, top-2, d_model 512, d_hidden 1024, one column per dropless_impl. More code paths, memory and the largest token counts are on the kernel page:
| dtype | tokens | op | Pallas (default) | blocked | ragged_dot |
|---|---|---|---|---|---|
| bf16 | 4,096 | forward | 0.857 | 2.35 | 6.69 |
| bf16 | 4,096 | train step | 2.73 | 5.69 | 19.2 |
| bf16 | 32,768 | forward | 5.06 | 15.6 | 48.8 |
| bf16 | 32,768 | train step | 15.9 | 36.4 | 144 |
| fp32 | 4,096 | forward | 1.29 | 2.37 | 6.66 |
| fp32 | 4,096 | train step | 4.64 | 5.80 | 19.4 |
| fp32 | 32,768 | forward | 9.29 | 15.6 | 48.9 |
| fp32 | 32,768 | train step | 31.9 | 36.4 | 146 |
Training-loop overhead¶
Per-step time of an MLP trained with a bare jitted step and with fit() on an in-memory array and on a stream. fit() recompiles on every call: a second call with the same model and data costs nearly as much wall time as the first (last two columns).
| device | width | bare jit (ms/step) | fit array (ms/step) |
fit stream (ms/step) |
fit first call (s) |
fit second call (s) |
|---|---|---|---|---|---|---|
| GPU | 64 | 0.159 | 0.205 | 0.378 | 0.60 | 0.36 |
| GPU | 512 | 0.459 | 0.532 | 0.636 | 0.85 | 0.65 |
| CPU | 64 | 0.142 | 0.130 | 0.227 | 0.40 | 0.29 |
| CPU | 512 | 23.4 | 13.4 | 17.7 | 3.09 | 2.97 |
How to reproduce¶
# Model grid (Transformer, DiffusionTransformer, SSM cells, UNet, MoE).
python -m docs.scripts.benchmark_gpu_nn --output docs/benchmarks/gpu_nn.json
# scan_layers / remat_policy, UNet variants, training loop; --peak adds device peaks.
python -m docs.scripts.benchmark_kernel_nn --device gpu --output docs/benchmarks/kernel_nn_gpu.json
JAX_PLATFORMS=cpu python -m docs.scripts.benchmark_kernel_nn --device cpu \
--output docs/benchmarks/kernel_nn_cpu.json
# Rebuild the figures and this page from the JSON.
python -m docs.scripts.benchmark_figures