Intermediate

JAX Core Concepts

Master JAX's fundamental building blocks: DeviceArrays, JIT compilation for speed, automatic differentiation with grad, and automatic vectorization with vmap.

DeviceArray (jax.Array)

JAX arrays work like NumPy arrays but live on accelerator devices (GPU/TPU) and are immutable:

Python
import jax.numpy as jnp

# Create arrays (just like NumPy!)
x = jnp.array([1.0, 2.0, 3.0])
zeros = jnp.zeros((3, 4))
rand = jax.random.normal(jax.random.PRNGKey(0), (3, 3))

# NumPy-like operations
y = jnp.dot(x, x)
z = jnp.sin(x) + jnp.cos(x)

# Immutable! Use .at[] for "updates"
# x[0] = 5  # ERROR! JAX arrays are immutable
x_new = x.at[0].set(5.0)  # Creates a new array

JIT Compilation

jax.jit compiles a function into optimized XLA code. The first call traces and compiles; subsequent calls run the compiled version:

Python
import jax

# Method 1: Decorator
@jax.jit
def fast_computation(x, y):
    return jnp.dot(x, y) + jnp.sum(x)

# Method 2: Wrap function
def slow_fn(x):
    return jnp.sum(jnp.sin(x) ** 2)

fast_fn = jax.jit(slow_fn)

# First call: compile + run (slower)
result = fast_fn(jnp.ones(1000000))

# Subsequent calls: just run (much faster!)
result = fast_fn(jnp.ones(1000000))
JIT Gotcha: JIT-compiled functions must have fixed control flow. Python if/else based on array values will not work inside jit. Use jax.lax.cond for conditional logic and jax.lax.fori_loop for loops.

Automatic Differentiation (grad)

jax.grad computes gradients of scalar-valued functions. It is the foundation of all training in JAX:

Python
import jax

# Gradient of a scalar function
def loss_fn(w, x, y):
    predictions = jnp.dot(x, w)
    return jnp.mean((predictions - y) ** 2)

# Compute gradient with respect to first argument (w)
grad_fn = jax.grad(loss_fn)
grads = grad_fn(w, x, y)

# Value and gradient together (more efficient)
loss, grads = jax.value_and_grad(loss_fn)(w, x, y)

# Higher-order derivatives
def f(x):
    return x ** 3

df = jax.grad(f)        # 3x^2
d2f = jax.grad(df)      # 6x
d3f = jax.grad(d2f)     # 6

print(d2f(2.0))  # 12.0 (6 * 2)

Vectorization (vmap)

jax.vmap automatically vectorizes a function to operate over batches. Write code for a single example, then vmap it to handle batches efficiently:

Python
import jax

# Function for a single data point
def predict_single(params, x):
    return jnp.dot(params['w'], x) + params['b']

# Automatically vectorize over a batch of inputs
predict_batch = jax.vmap(predict_single, in_axes=(None, 0))
# None = don't vectorize params, 0 = vectorize x along first axis

# Works on entire batches!
params = {'w': jnp.ones(10), 'b': 0.0}
batch_x = jnp.ones((32, 10))  # 32 samples
predictions = predict_batch(params, batch_x)  # (32,)

# Compose with other transforms
fast_batch_predict = jax.jit(jax.vmap(predict_single, in_axes=(None, 0)))

# Per-sample gradients (impossible in PyTorch without tricks!)
per_sample_grads = jax.vmap(jax.grad(loss_fn), in_axes=(None, 0, 0))
vmap is a superpower: In PyTorch, computing per-sample gradients requires tricks or external libraries. In JAX, it is just jax.vmap(jax.grad(fn)). This composability is what makes JAX uniquely powerful.

Next Up: Neural Networks

Now that you understand JAX's core transforms, let's build neural networks with Flax and Haiku and train them with Optax.

Next: Neural Networks →

Ready to Go Deeper?

Live instructor-led courses from our partners. Affiliate disclosure.