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

Nested vmap and Advanced Patterns

~9 min · vmap, jax, tutorial

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

You can nest vmap calls to vectorize over multiple dimensions. Each vmap adds one batch dimension.

import jax
import jax.numpy as jnp

def pairwise_distance(a, b):
    """Euclidean distance between two vectors."""
    return jnp.sqrt(jnp.sum((a - b) ** 2))

# Vectorize over pairs: distance between corresponding rows
batched_distance = jax.vmap(pairwise_distance)

A = jnp.array([[1.0, 0.0], [3.0, 0.0], [0.0, 4.0]])
B = jnp.array([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]])

print(batched_distance(A, B))  # [1.0, 3.0, 4.0]

# Double vmap: pairwise distance MATRIX (every a against every b)
# Outer vmap: over rows of A
# Inner vmap: over rows of B
distance_matrix = jax.vmap(jax.vmap(pairwise_distance, in_axes=(None, 0)), in_axes=(0, None))

D = distance_matrix(A, B)
print(D.shape)  # (3, 3)
print(D)
# [[1.    0.    0.   ]   distances from A[0] to each B
#  [3.    0.    0.   ]   distances from A[1] to each B
#  [4.    0.    0.   ]]  distances from A[2] to each B

vmap + jit: Maximum Performance

import jax
import jax.numpy as jnp

def process_single(params, x):
    """Complex per-example computation."""
    h = jnp.tanh(x @ params['w1'] + params['b1'])
    return jnp.sum(h ** 2)

# Stack all transformations: vectorize, then compile
process_batch = jax.jit(jax.vmap(process_single, in_axes=(None, 0)))

# Create params as a dict (pytree)
key = jax.random.PRNGKey(0)
k1, k2 = jax.random.split(key)
params = {
    'w1': jax.random.normal(k1, (4, 8)),
    'b1': jnp.zeros(8),
}

X = jax.random.normal(k2, (128, 4))  # batch of 128

result = process_batch(params, X)
print(result.shape)  # (128,) — one scalar per example

Data Augmentation with vmap

import jax
import jax.numpy as jnp

def augment_single(key, image):
    """Random augmentation for a single image."""
    k1, k2 = jax.random.split(key)
    # Random brightness
    brightness = jax.random.uniform(k1, minval=0.8, maxval=1.2)
    # Random noise
    noise = 0.05 * jax.random.normal(k2, image.shape)
    return jnp.clip(image * brightness + noise, 0.0, 1.0)

# Vectorize over a batch of (key, image) pairs
augment_batch = jax.jit(jax.vmap(augment_single))

# Each image gets its own random key for independent augmentation
master_key = jax.random.PRNGKey(42)
batch_size = 32
keys = jax.random.split(master_key, batch_size)
images = jax.random.uniform(jax.random.PRNGKey(0), (batch_size, 28, 28))

augmented = augment_batch(keys, images)
print(augmented.shape)  # (32, 28, 28)

💡 Why This Matters

The vmap + explicit PRNG key pattern for data augmentation is both elegant and correct. Each example gets its own random key, ensuring independent random transformations. And because it's all vmapped and JIT-compiled, it runs at full hardware speed. This pattern scales naturally — vmap over examples, then pmap over devices.

Code

import jax
import jax.numpy as jnp

def pairwise_distance(a, b):
    """Euclidean distance between two vectors."""
    return jnp.sqrt(jnp.sum((a - b) ** 2))

# Vectorize over pairs: distance between corresponding rows
batched_distance = jax.vmap(pairwise_distance)

A = jnp.array([[1.0, 0.0], [3.0, 0.0], [0.0, 4.0]])
B = jnp.array([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]])

print(batched_distance(A, B))  # [1.0, 3.0, 4.0]

# Double vmap: pairwise distance MATRIX (every a against every b)
# Outer vmap: over rows of A
# Inner vmap: over rows of B
distance_matrix = jax.vmap(jax.vmap(pairwise_distance, in_axes=(None, 0)), in_axes=(0, None))

D = distance_matrix(A, B)
print(D.shape)  # (3, 3)
print(D)
# [[1.    0.    0.   ]   distances from A[0] to each B
#  [3.    0.    0.   ]   distances from A[1] to each B
#  [4.    0.    0.   ]]  distances from A[2] to each B
import jax
import jax.numpy as jnp

def process_single(params, x):
    """Complex per-example computation."""
    h = jnp.tanh(x @ params['w1'] + params['b1'])
    return jnp.sum(h ** 2)

# Stack all transformations: vectorize, then compile
process_batch = jax.jit(jax.vmap(process_single, in_axes=(None, 0)))

# Create params as a dict (pytree)
key = jax.random.PRNGKey(0)
k1, k2 = jax.random.split(key)
params = {
    'w1': jax.random.normal(k1, (4, 8)),
    'b1': jnp.zeros(8),
}

X = jax.random.normal(k2, (128, 4))  # batch of 128

result = process_batch(params, X)
print(result.shape)  # (128,) — one scalar per example
import jax
import jax.numpy as jnp

def augment_single(key, image):
    """Random augmentation for a single image."""
    k1, k2 = jax.random.split(key)
    # Random brightness
    brightness = jax.random.uniform(k1, minval=0.8, maxval=1.2)
    # Random noise
    noise = 0.05 * jax.random.normal(k2, image.shape)
    return jnp.clip(image * brightness + noise, 0.0, 1.0)

# Vectorize over a batch of (key, image) pairs
augment_batch = jax.jit(jax.vmap(augment_single))

# Each image gets its own random key for independent augmentation
master_key = jax.random.PRNGKey(42)
batch_size = 32
keys = jax.random.split(master_key, batch_size)
images = jax.random.uniform(jax.random.PRNGKey(0), (batch_size, 28, 28))

augmented = augment_batch(keys, images)
print(augmented.shape)  # (32, 28, 28)

External links

Exercise

Build a 2D outer product f(x, y) = x * y over (1-D, 1-D) → 2-D matrix using nested vmap, no jnp.outer. Time it against jnp.outer for size 1024×1024. Note when nested vmap hits its limit and you should reach for jnp.einsum.

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.