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

Profiling and Debugging Performance

~8 min · ecosystem, jax, tutorial

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

When your JAX code is slow, you need to figure out why. JAX provides several profiling tools to identify bottlenecks.

Basic Profiling

import jax
import jax.numpy as jnp

# Check what's being compiled
@jax.jit
def my_function(x):
    return jnp.sin(x) + jnp.cos(x)

# Inspect the jaxpr (JAX's intermediate representation)
jaxpr = jax.make_jaxpr(my_function)(jnp.ones(3))
print(jaxpr)
# Shows the operations JAX will compile — useful for checking
# that your function isn't doing unexpected work

# Inspect compiled HLO
compiled = jax.jit(my_function).lower(jnp.ones(3)).compile()
print(compiled.cost_analysis())
# Shows estimated FLOPs, memory usage, etc.

TensorBoard Profiling

import jax

# Profile with TensorBoard integration
jax.profiler.start_trace('/tmp/jax_profile')

# Run your training code
for step in range(100):
    params, loss = train_step(params, batch)

jax.profiler.stop_trace()
# Then: tensorboard --logdir=/tmp/jax_profile

# Or use the context manager
with jax.profiler.trace('/tmp/jax_profile'):
    for step in range(100):
        params, loss = train_step(params, batch)

Detecting Recompilation

# Set this to log when JIT recompiles
jax.config.update("jax_log_compiles", True)

# Now you'll see messages like:
# "Compiling my_function (..." every time JIT compiles a new variant

# Check XLA compilation logs for detailed info
# JAX_LOG_COMPILES=1 python my_script.py
# Timing best practices
import time

@jax.jit
def fast_fn(x):
    return jnp.linalg.svd(x, full_matrices=False)

x = jax.random.normal(jax.random.key(0), (1000, 500))

# First call includes compilation time
start = time.time()
result = fast_fn(x)
jax.block_until_ready(result)
print(f"First call (includes compile): {time.time() - start:.4f}s")

# Second call is the real runtime
start = time.time()
result = fast_fn(x)
jax.block_until_ready(result)
print(f"Second call (actual runtime): {time.time() - start:.4f}s")

💡 Why This Matters

JAX's lazy execution model means that computation doesn't happen when you call a function — it happens when you need the result. This makes naive timing misleading (you're timing dispatch, not computation). Always use block_until_ready() for timing, and always run twice — the first call includes compilation time. The TensorBoard profiler shows exactly where time is spent: compilation, data transfer, or actual computation.

Code

import jax
import jax.numpy as jnp

# Check what's being compiled
@jax.jit
def my_function(x):
    return jnp.sin(x) + jnp.cos(x)

# Inspect the jaxpr (JAX's intermediate representation)
jaxpr = jax.make_jaxpr(my_function)(jnp.ones(3))
print(jaxpr)
# Shows the operations JAX will compile — useful for checking
# that your function isn't doing unexpected work

# Inspect compiled HLO
compiled = jax.jit(my_function).lower(jnp.ones(3)).compile()
print(compiled.cost_analysis())
# Shows estimated FLOPs, memory usage, etc.
import jax

# Profile with TensorBoard integration
jax.profiler.start_trace('/tmp/jax_profile')

# Run your training code
for step in range(100):
    params, loss = train_step(params, batch)

jax.profiler.stop_trace()
# Then: tensorboard --logdir=/tmp/jax_profile

# Or use the context manager
with jax.profiler.trace('/tmp/jax_profile'):
    for step in range(100):
        params, loss = train_step(params, batch)
# Set this to log when JIT recompiles
jax.config.update("jax_log_compiles", True)

# Now you'll see messages like:
# "Compiling my_function (..." every time JIT compiles a new variant

# Check XLA compilation logs for detailed info
# JAX_LOG_COMPILES=1 python my_script.py
# Timing best practices
import time

@jax.jit
def fast_fn(x):
    return jnp.linalg.svd(x, full_matrices=False)

x = jax.random.normal(jax.random.key(0), (1000, 500))

# First call includes compilation time
start = time.time()
result = fast_fn(x)
jax.block_until_ready(result)
print(f"First call (includes compile): {time.time() - start:.4f}s")

# Second call is the real runtime
start = time.time()
result = fast_fn(x)
jax.block_until_ready(result)
print(f"Second call (actual runtime): {time.time() - start:.4f}s")

External links

Exercise

Profile a 50-step training run with jax.profiler.start_trace. Open the trace in TensorBoard or perfetto. Find the single slowest op. Optimize one thing — even just rearranging a matmul. Profiling first, optimizing second.

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.