C.W.K.
Stream
Lesson 03 of 04 · published

vmap + grad: Per-Example Gradients

~9 min · vmap, jax, tutorial

Level 0Curious
0 XP0/73 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

The combination of vmap and grad is JAX's flagship feature. It lets you compute individual gradients for each example in a batch — something that's naturally impossible in standard backpropagation (which computes the average gradient).

import jax
import jax.numpy as jnp

def loss_single(params, x, y):
    """Loss for a SINGLE example — keep it simple!"""
    pred = jnp.dot(x, params)
    return (pred - y) ** 2

# Regular gradient: gradient of average loss
def batch_loss(params, X, y):
    return jnp.mean(jax.vmap(loss_single, in_axes=(None, 0, 0))(params, X, y))

avg_grad = jax.grad(batch_loss)

# Per-example gradients: separate gradient for each example
per_example_grad = jax.vmap(jax.grad(loss_single), in_axes=(None, 0, 0))

# Test
params = jnp.array([1.0, 2.0, 3.0])
X = jax.random.normal(jax.random.PRNGKey(0), (32, 3))
y = jax.random.normal(jax.random.PRNGKey(1), (32,))

# Average gradient: shape (3,)
g_avg = avg_grad(params, X, y)
print(f"Average gradient shape: {g_avg.shape}")  # (3,)

# Per-example gradients: shape (32, 3)
g_per = per_example_grad(params, X, y)
print(f"Per-example gradient shape: {g_per.shape}")  # (32, 3)

# Verify: mean of per-example gradients ≈ average gradient
print(f"Match: {jnp.allclose(jnp.mean(g_per, axis=0), g_avg, atol=1e-5)}")

Application: Differential Privacy

Differential privacy requires clipping individual gradients before averaging. Without per-example gradients, this is computationally expensive. With vmap + grad, it's straightforward:

import jax
import jax.numpy as jnp

def loss_single(params, x, y):
    pred = jnp.dot(x, params)
    return (pred - y) ** 2

def dp_gradient(params, X, y, clip_norm=1.0, noise_scale=0.1, key=None):
    """Differentially private gradient computation."""
    # 1. Get per-example gradients
    per_ex_grads = jax.vmap(
        jax.grad(loss_single), in_axes=(None, 0, 0)
    )(params, X, y)

    # 2. Clip each gradient to the norm bound
    grad_norms = jnp.linalg.norm(per_ex_grads, axis=-1, keepdims=True)
    clip_factor = jnp.minimum(1.0, clip_norm / (grad_norms + 1e-8))
    clipped_grads = per_ex_grads * clip_factor

    # 3. Average and add noise
    avg_grad = jnp.mean(clipped_grads, axis=0)
    noise = noise_scale * clip_norm * jax.random.normal(key, avg_grad.shape)
    return avg_grad + noise / X.shape[0]

# Usage
key = jax.random.PRNGKey(42)
params = jnp.array([1.0, 2.0, 3.0])
X = jax.random.normal(jax.random.PRNGKey(0), (64, 3))
y = jax.random.normal(jax.random.PRNGKey(1), (64,))

dp_grad = dp_gradient(params, X, y, key=key)
print(f"DP gradient: {dp_grad}")

💡 Why This Matters

Per-example gradients were historically very expensive — you had to process one example at a time. PyTorch's torch.func.vmap and torch.func.grad (introduced in PyTorch 2.0) were directly inspired by JAX's vmap + grad composition. This pattern is the foundation of efficient differential privacy, per-example gradient analysis, and Fisher information computation.

Code

import jax
import jax.numpy as jnp

def loss_single(params, x, y):
    """Loss for a SINGLE example — keep it simple!"""
    pred = jnp.dot(x, params)
    return (pred - y) ** 2

# Regular gradient: gradient of average loss
def batch_loss(params, X, y):
    return jnp.mean(jax.vmap(loss_single, in_axes=(None, 0, 0))(params, X, y))

avg_grad = jax.grad(batch_loss)

# Per-example gradients: separate gradient for each example
per_example_grad = jax.vmap(jax.grad(loss_single), in_axes=(None, 0, 0))

# Test
params = jnp.array([1.0, 2.0, 3.0])
X = jax.random.normal(jax.random.PRNGKey(0), (32, 3))
y = jax.random.normal(jax.random.PRNGKey(1), (32,))

# Average gradient: shape (3,)
g_avg = avg_grad(params, X, y)
print(f"Average gradient shape: {g_avg.shape}")  # (3,)

# Per-example gradients: shape (32, 3)
g_per = per_example_grad(params, X, y)
print(f"Per-example gradient shape: {g_per.shape}")  # (32, 3)

# Verify: mean of per-example gradients ≈ average gradient
print(f"Match: {jnp.allclose(jnp.mean(g_per, axis=0), g_avg, atol=1e-5)}")
import jax
import jax.numpy as jnp

def loss_single(params, x, y):
    pred = jnp.dot(x, params)
    return (pred - y) ** 2

def dp_gradient(params, X, y, clip_norm=1.0, noise_scale=0.1, key=None):
    """Differentially private gradient computation."""
    # 1. Get per-example gradients
    per_ex_grads = jax.vmap(
        jax.grad(loss_single), in_axes=(None, 0, 0)
    )(params, X, y)

    # 2. Clip each gradient to the norm bound
    grad_norms = jnp.linalg.norm(per_ex_grads, axis=-1, keepdims=True)
    clip_factor = jnp.minimum(1.0, clip_norm / (grad_norms + 1e-8))
    clipped_grads = per_ex_grads * clip_factor

    # 3. Average and add noise
    avg_grad = jnp.mean(clipped_grads, axis=0)
    noise = noise_scale * clip_norm * jax.random.normal(key, avg_grad.shape)
    return avg_grad + noise / X.shape[0]

# Usage
key = jax.random.PRNGKey(42)
params = jnp.array([1.0, 2.0, 3.0])
X = jax.random.normal(jax.random.PRNGKey(0), (64, 3))
y = jax.random.normal(jax.random.PRNGKey(1), (64,))

dp_grad = dp_gradient(params, X, y, key=key)
print(f"DP gradient: {dp_grad}")

External links

Exercise

Compute per-example gradients on a batch of 256 examples. Verify the average matches the gradient of the mean-loss. Then plot a histogram of gradient norms — that's an actual diagnostic real research uses.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.