JAX Autodiff and JIT Compilation Explained

JAX transforms standard Python and NumPy code into high-performance, hardware-accelerated machine learning pipelines. This article explores what makes JAX fundamentally distinct in its ability to automatically differentiate and Just-In-Time (JIT) compile Python functions. By utilizing functional program transformations, an intermediate tracing representation called jaxpr, and the XLA (Accelerated Linear Algebra) compiler, JAX unifies flexibility and speed without requiring custom graph-building syntax.

Composable Function Transformations

Traditional deep learning frameworks like PyTorch use tape-based automatic differentiation, constructing a dynamic computation graph on the fly during each forward pass. In contrast, JAX treats code transformation as a first-class citizen.

Instead of wrapping tensors in graph-tracking objects, JAX exposes pure functions that take functions as inputs and return new functions as outputs. The primary transformation primitives include:

Because these transformations operate on functions rather than objects, they are completely composable. You can nest them in any order, such as jit(vmap(grad(f))), enabling higher-order derivatives and parallel batching without modifying the original implementation of the function f.

Tracing and Jaxpr Representation

JAX does not parse Python abstract syntax trees (AST) or interpret raw Python bytecode to perform differentiation or compilation. Instead, it relies on a process known as tracing.

When a transformed function is executed, JAX passes abstract tracer values through the function instead of concrete numerical arrays. As Python executes, these tracers record every primitive numerical operation encountered.

This recording generates an intermediate representation called a jaxpr (JAX expression). A jaxpr is a typed, purely functional directed acyclic graph (DAG) representing the mathematical operations performed by the Python function. It strips away standard Python control flow, runtime overhead, and non-array-related logic, leaving only pure mathematical primitives ready for transformation.

Native Autodiff via Vector-Jacobian Products

Automatic differentiation in JAX operates directly on the primitive operations defined in the jaxpr. Every primitive in JAX has registered rules for:

  1. Forward-mode differentiation via Jacobian-Vector Products (JVPs).
  2. Reverse-mode differentiation via Vector-Jacobian Products (VJPs).

When you call jax.grad(f), JAX traces f into a jaxpr, decomposes the operations into their elementary components, and applies reverse-mode differentiation rules via the chain rule. Because the output of grad is itself a valid Python function that generates its own jaxpr, taking higher-order derivatives like Hessians simply requires calling jax.grad(jax.grad(f)).

Compilation with XLA

Once JAX captures a function's computational graph in a jaxpr, it bypasses the standard Python interpreter using the jax.jit transformation.

jax.jit lowers the jaxpr into the High-Level Optimizer (HLO) format consumed by Google's XLA (Accelerated Linear Algebra) compiler. XLA performs aggressive hardware-specific optimizations for CPUs, GPUs, and TPUs, including:

The compiled artifact runs directly on the hardware accelerator as native machine code, completely detached from the Python Global Interpreter Lock (GIL).

The Pure Function Constraint

The mechanism that enables JAX’s distinct autodiff and JIT compilation capabilities requires a strict programming model: functions must be functionally pure.

A pure function must:

Because JAX executes Python code only during the tracing phase, any side effects (such as standard print statements or appending to a Python list) occur once during tracing and will not execute in subsequent calls to the compiled binary. By enforcing functional purity, JAX guarantees that traced computation graphs remain strictly deterministic, enabling seamless mathematical transformation and compilation.