Skip to content

JIT and autodiff

Operators are JAX pytrees, so the transformations work without special handling.

jit

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
dense = jax.random.normal(key, (4, 4))
spd = dense @ dense.T + 4 * jnp.eye(4)

@jax.jit
def solve_it(matrix, rhs):
    return linox.solve(linox.Matrix(matrix), rhs)

assert solve_it(spd, jnp.ones(4)).shape == (4,)

An operator can also cross the boundary as an argument, because it flattens into its arrays:

import jax
import jax.numpy as jnp
import linox

op = linox.Diagonal(jnp.arange(1.0, 5.0))

@jax.jit
def apply(operator, vector):
    return operator @ vector

assert jnp.allclose(apply(op, jnp.ones(4)), jnp.arange(1.0, 5.0))

grad and vmap

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
dense = jax.random.normal(key, (4, 4))
spd = dense @ dense.T + 4 * jnp.eye(4)
b = jnp.ones(4)

wrt_rhs = jax.grad(lambda v: linox.solve(linox.Matrix(spd), v).sum())(b)
wrt_matrix = jax.grad(lambda m: linox.solve(linox.Matrix(m), b).sum())(spd)
over_batch = jax.vmap(lambda v: linox.solve(linox.Matrix(spd), v))(jnp.ones((5, 4)))

assert wrt_rhs.shape == (4,)
assert wrt_matrix.shape == (4, 4)
assert over_batch.shape == (5, 4)

What changes under a trace

Two things behave differently, and both are deliberate.

Checks become runtime errors. A precondition cannot be raised at trace time, because the condition is a tracer with no truth value. Rather than skip the check — which leaves it absent exactly where mistakes are hardest to see — linox defers to a runtime callback, and a violation surfaces when the computation executes.

Structural decisions are made at trace time. Which dispatch runs, and which rewrite applies, is fixed when the function is traced. That is why they depend on operator types and static shapes, never on values.

Iterative solvers and reverse mode

lax.while_loop has no reverse-mode rule, so an iterative solver that early-exits cannot be differentiated directly. CG routes its solution through lax.custom_linear_solve, which supplies the adjoint — for a symmetric operator the cotangent is itself a solve:

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
q, _ = jnp.linalg.qr(jax.random.normal(key, (20, 20)))
spd = q @ jnp.diag(jnp.linspace(1.0, 5.0, 20)) @ q.T
b = jnp.ones(20)

g = jax.grad(lambda v: linox.solve(linox.Matrix(spd), v, method="cg").sum())(b)
assert jnp.allclose(g, jnp.linalg.solve(spd, jnp.ones(20)), atol=1e-6)

The trade is that the iteration count is then not observable. cg_solve offers both: track_iterations=True reports the exact count and gives up reverse mode. See Solving.