| name | jax |
| description | [Applies to: **/*.py] Definitive guidelines for writing high-performance, functionally pure, and maintainable JAX code, focusing on common pitfalls and optimal patterns for accelerators. |
| source | cursor_mdc |
jax Best Practices
JAX is the backbone of our AI/ML and numerical computing projects. Adhere to these principles for high-performance, reproducible, and maintainable JAX code.
1. Functional Purity: The Absolute Core
JAX transformations (jit, grad, vmap, pmap) operate exclusively on functionally pure code. This means functions must be free of side-effects: all inputs explicit, all results returned.
-
Avoid mutable global state: JAX captures global values at first jit compilation, leading to stale values.
❌ BAD:
g = 0
def impure_uses_globals(x):
return x + g
✅ GOOD: Pass all state explicitly.
def pure_uses_globals(x, g_val):
return x + g_val
-
No in-place array mutation: JAX arrays are immutable. Use the .at[] syntax for functional updates.
❌ BAD:
import jax.numpy as jnp
arr = jnp.zeros((3,3))
arr[1, :] = 1.0
✅ GOOD:
import jax.numpy as jnp
arr = jnp.zeros((3,3))
updated_arr = arr.at[1, :].set(1.0)
-
Avoid Python iterators in jitted code: Iterators introduce state.
❌ BAD:
from jax import jit
def sum_iterator(it):
total = 0
for x in it:
total += x
return total
✅ GOOD: Use JAX control flow primitives.
from jax import lax
import jax.numpy as jnp
def sum_array(arr):
return lax.fori_loop(0, arr.shape[0], lambda i, x: x + arr[i], 0)
2. Numerical Type Discipline
Prioritize float32 for performance on accelerators. Avoid implicit float64 promotion.
- Explicit
dtype for constants:
❌ BAD: Implicitly typed Python floats can lead to float64 promotion.
import jax.numpy as jnp
x = jnp.ones(5, dtype=jnp.float32)
y = x * 2.0
✅ GOOD: Use 0-D jnp.array with explicit dtype or jnp.float32().
import jax.numpy as jnp
x = jnp.ones(5, dtype=jnp.float32)
y = x * jnp.array(2.0, dtype=jnp.float32)
z = x * jnp.float32(2.0)
3. Performance Considerations: Stable Compilation
Prevent costly recompilations and leverage XLA effectively.
-
Static shapes: Keep input shapes static or pass them via static_argnums to jit. Dynamic shapes force recompilation.
❌ BAD:
from jax import jit
def dynamic_shape_func(x):
return x.sum()
✅ GOOD:
from jax import jit
@jit
def static_shape_func(x):
return x.sum()
If shapes must vary, consider static_argnums for non-array arguments that determine shape.
-
JAX control flow primitives: Always use lax.scan, lax.while_loop, lax.cond inside jitted functions. Python control flow breaks XLA compilation.
❌ BAD:
from jax import jit
@jit
def python_loop(x, n):
for _ in range(n):
x = x * 2
return x
✅ GOOD:
from jax import jit, lax
():
lax.fori_loop(, n, i, val: val * , x)
4. Code Organization and Randomness
-
Standard Imports:
import jax
import jax.numpy as jnp
import jax.random as jr
import jax.lax as lax
-
Randomness Management: Use jax.random and explicitly split PRNGKeys. Never reuse a key.
❌ BAD:
import jax.random as jr
key = jr.PRNGKey(0)
val1 = jr.normal(key, (5,))
val2 = jr.normal(key, (5,))
✅ GOOD:
import jax.random as jr
key = jr.PRNGKey(0)
key, subkey1 = jr.split(key)
val1 = jr.normal(subkey1, (5,))
key, subkey2 = jr.split(key)
val2 = jr.normal(subkey2, (5,))
5. Type Hints
Use standard Python type hints, especially jax.Array for JAX arrays. This improves readability and enables static analysis.
import jax.numpy as jnp
from jax import Array, jit
@jit
def add_arrays(a: Array, b: Array) -> Array:
"""Adds two JAX arrays."""
return a + b
6. Testing Approaches
pytest is standard: Use pytest for unit and integration tests.
- Gradient checking: For custom operations or complex functions, use
jax.test_util.check_grads to verify gradients.
import jax
import jax.numpy as jnp
from jax.test_util import check_grads
def my_complex_func(x):
return jnp.sin(x) * jnp.exp(x)