The Jacobian may be dense, but the program never was.
Advice from under my shell: never form .
Group the columns that never share a row. Snails prefer their own lane too.
When the Jacobian Is Dense but the Program Is Not
In machine learning, automatic differentiation is mostly used for products with the Jacobian: the gradient of a loss is a single vector-Jacobian product, and training never forms the Jacobian itself. Many methods in scientific computing need the matrix. Newtonβs method for a nonlinear system , at the heart of implicit time stepping for stiff differential equations and of steady-state solvers for discretized partial differential equations, factorizes the Jacobian in every step, and solvers for constrained optimization, such as Ipopt [1] and Uno [2], take the Jacobian of the constraints and the Hessian of the Lagrangian as matrices. These Jacobians are large, with thousands to millions of unknowns, but more often than not sparse, since each equation involves only a few unknowns.
An Illustrated Guide to Automatic Sparse Differentiation [3] explains how standard automatic differentiation tools can be used to compute a sparse Jacobian at a fraction of the cost of a dense one. The idea is fairly simple: first detect which entries can be nonzero, group the columns (or rows) that never share a row (or column), and compute Jacobian-vector (or vector-Jacobian) products in order to generate the sparse Jacobian.11 The grouping is for efficiency: all columns of a group are computed by a single Jacobian-vector product, and all rows of a group by a single vector-Jacobian product. In Backpropagation as a Linear Solve we derived that the Jacobian of any computation can be computed as
(1)
where , , are matrices which more often than not are sparse and with further being unit lower triangular. Even when is dense, products with it are cheap: is evaluated from right to left, with one forward substitution in the sparse, unit lower triangular , at about the cost of running the program.22 Likewise for the transpose: needs one backward substitution in the unit upper triangular , which is reverse mode. What a dense makes expensive is forming it, or solving with it.
This post asks what the matrices , , and add to the understanding of the Jacobian. The answer in short is that they pay off when is sparse but is dense, resulting in a dense Jacobian (e.g., Example 1).
ExampleΒ 1: A cumulative sum
Example 1 (A cumulative sum)Β Β We will explain how to compute , , and in Recap 1, but for now just trust the process.
For the scaled cumulative sum of exponentials, with constants , computed by , is lower bidiagonal, and with and are diagonal. The inverse of is dense,
(2)
and so is the Jacobian . However, often the dense Jacobian need not be explicitly formed. A product with , or a solve with it, can be written as one sparse system,
(3)
whose first block row gives and second . This augmented system keeps the sparsity of , , and however dense is, and it is the subject of this post.
RecapΒ 1: The three matrices
Recap 1 (The three matrices)Β Β This recaps the derivation of Backpropagation as a Linear Solve, written for a general program instead of a network. A program computes by a sequence of elementary operations, such as a product, a sum, or an exponential. Each operation in any program produces an intermediate variable from earlier intermediate variables and the inputs 33 That it only depends on previous nodes gives a directed acyclic graph, which we here represent as a lower triangular matrix.. Each intermediate variable becomes a node in the computational graph of the program, with an edge into node from every and that actually depends on. Collecting the nodes gives the forward relation . Most of the time weβre only interested in a selection of the intermediate computation, so we introduce a matrix , usually a selection with entries 0 and 1, so that . Differentiating the forward relation gives , that is, with
(4)
and therefore . Since a node reads only earlier nodes, is strictly lower triangular and is unit lower triangular, and since it reads only a few of them, and have only a few nonzeros per row.
ExampleΒ 2: Four nodes
Example 2 (Four nodes)Β Β To compute , a program introduces one intermediate variable per elementary operation, four in total: , , , and . The forward relation and the outputs are
(5)
Row of is the operation that computes the intermediate variable . Implicitly, defines a computational graph, as every intermediate variable depends on only a few earlier and inputs . The Jacobian of with respect to all of its arguments splits into the two blocks of partial derivatives
(6)
The intermediate variables depend on through the forward relation itself, , so the chain rule gives
(7)
In our example the unit lower triangular matrix is44 Writing with strictly lower triangular, is nilpotent, , so the Neumann series of terminates, . Here , since depends only on intermediate variables that depend on no others, so .
(8)
Solving with one intermediate variable at a time, by forward substitution, gives
(9)
The Jacobian of picks out the rows of the outputs,
(10)
1. From program to matrices
We can create , , and automatically by recording the program. A tape stores the partial derivatives of every operation, evaluated as the program runs; the intermediate values are only needed during the run, to compute those partial derivatives and the outputs. The tape of this post is deliberately small. A Tracked number holds a value and the index of its node. Arithmetic on Tracked numbers is overloaded, as for dual numbers: a * b computes the product, gives it the next node index, and stores its partial derivatives, b with respect to a and a with respect to b, as entries of or , depending on whether a and b are nodes or inputs. The function trace runs on Tracked inputs and returns together with the sparse matrices , , and .
CodeΒ 1: A scalar tape in Julia
Code 1 (A scalar tape in Julia)Β Β
1# A scalar tape. Every operation on a Tracked number is one node Ξ¦α΅’2# of the forward relation u = Ξ¦(u, x), and it stores its partial3# derivatives, the entries L[i, j] = -βΞ¦α΅’/βuβ±Ό and Mα΅[i, k] = βΞ¦α΅’/βxβ.4using LinearAlgebra, SparseArrays56# The tape: rows, columns and values of the entries of L - I and Mα΅,7# and the number of nodes8const L_entries = (Int[], Int[], Float64[])9const Mt_entries = (Int[], Int[], Float64[])10const nodes = Ref(0)1112# index > 0 is the node uα΅’, index < 0 is the input xβα΅’13struct Tracked14 value::Float6415 index::Int16end1718# Append the node with the given value and (argument => partial) pairs19function record!(value, args...)20 i = nodes[] += 121 for (a, βa) in args22 rows, cols, vals = a.index > 0 ? L_entries : Mt_entries23 push!(rows, i)24 push!(cols, abs(a.index))25 push!(vals, a.index > 0 ? -βa : βa)26 end27 return Tracked(value, i)28end2930Base.:+(a::Tracked, b::Tracked) = record!(a.value + b.value, a => 1.0, b => 1.0)31Base.:-(a::Tracked, b::Tracked) = record!(a.value - b.value, a => 1.0, b => -1.0)32Base.:*(a::Tracked, b::Tracked) = record!(a.value * b.value, a => b.value, b => a.value)33Base.:/(a::Tracked, b::Tracked) =34 record!(a.value / b.value, a => 1 / b.value, b => -a.value / b.value^2)35Base.:+(a::Tracked, c::Real) = record!(a.value + c, a => 1.0)36Base.:+(c::Real, a::Tracked) = a + c37Base.:-(a::Tracked, c::Real) = a + (-c)38Base.:-(c::Real, a::Tracked) = record!(c - a.value, a => -1.0)39Base.:-(a::Tracked) = record!(-a.value, a => -1.0)40Base.:*(a::Tracked, c::Real) = record!(a.value * c, a => Float64(c))41Base.:*(c::Real, a::Tracked) = a * c42Base.:/(a::Tracked, c::Real) = a * inv(c)43Base.:/(c::Real, a::Tracked) = record!(c / a.value, a => -c / a.value^2)44Base.exp(a::Tracked) = (v = exp(a.value); record!(v, a => v))45Base.sin(a::Tracked) = record!(sin(a.value), a => cos(a.value))46Base.:^(a::Tracked, p::Integer) = record!(a.value^p, a => p * a.value^(p - 1))4748# Run f on tracked inputs and return f(x) with the sparse matrices49# L = I - βΞ¦/βu, Mα΅ = βΞ¦/βx and the selection P of the outputs, so50# that the Jacobian of f at x is P Lβ»ΒΉ Mα΅.51function trace(f, x)52 foreach(empty!, (L_entries..., Mt_entries...))53 nodes[] = 054 y = f([Tracked(xk, -k) for (k, xk) in enumerate(x)])55 d, n, m = nodes[], length(x), length(y)56 L = I + sparse(L_entries..., d, d) # sums the entries of x * x57 Mt = sparse(Mt_entries..., d, n)58 P = sparse(1:m, [yi.index for yi in y], ones(m), m, d)59 return [yi.value for yi in y], L, Mt, P60endWith the matrices at hand, the Jacobian itself is a triangular solve with a sparse right-hand side, . Column of is nonzero only at the intermediate variables that depend on , and a sparse triangular solve finds those by a search through the graph of before it does any arithmetic. SparseSparse.jl does this when both sides of \ are sparse, whereas the standard library returns a dense matrix.
CodeΒ 2: A Jacobian by a sparse solve
Code 2 (A Jacobian by a sparse solve)Β Β
1using SparseSparse # Required for \ with a sparse right-hand side2include("tape.jl")34# u''(xα΅’) by central differences, with u = 0 at both ends5function cdiff2(x, i, h)6 left = i > 1 ? x[i-1] : 0.07 right = i < length(x) ? x[i+1] : 0.08 return (left - 2x[i] + right) / h^29end1011# Bratu, u'' + Ξ» exp(u) = 0: each output reads three inputs12function bratu(x; Ξ»=1.0)13 h = 1 / (length(x) + 1)14 return [cdiff2(x, i, h) + Ξ» * exp(x[i]) for i in eachindex(x)]15end1617# Nonlocal Ohmic heating, u'' + Ξ» exp(-u) / (β«βΒΉ exp(-u) dx)Β² = 0, of the18# next section: every output reads the sum s, so every input19function heating(x; Ξ»=2.0)20 h = 1 / (length(x) + 1)21 s = sum(exp(-xj) for xj in x) # h * s is the integral22 scaling = Ξ» / (h * s)^223 return [cdiff2(x, i, h) + scaling * exp(-x[i]) for i in eachindex(x)]24end2526x = 0.1 .* rand(1000)27_, L, Mt, P = trace(bratu, x) # nnz(L) = 13000, nnz(M) = 399828J = P * (L \ Mt) # tridiagonal: nnz(J) = 299829_, L, Mt, P = trace(heating, x) # nnz(L) = 27997, nnz(M) = 499830J = P * (L \ Mt) # dense: nnz(J) = 10000002. The augmented system
While both the Bratu and the heating residual (Example 3) in Code 2 give sparse , , and , only the Bratu Jacobian is sparse. The cumulative sum of Example 1 already showed how this can happen: a sparse can have a dense inverse, in which case sparse differentiation cannot help. For products, a dense is no problem, since costs one forward substitution. Newtonβs method for , however, needs a solve, , in every step, and with a dense that means forming and factorizing it, at a cost of order . The cumulative sum also hinted at the way around this: given , , and , the solve can be written as one sparse augmented system,
(11)
whose first block row gives and second . Newtonβs method can therefore solve with the sparse , whose unknowns are and the step , instead of with the dense .55 The same factorization of also solves with , as in adjoint methods: is , the system with .
ExampleΒ 3: Ohmic heating
Example 3 (Ohmic heating)Β Β A nonlocal model of a resistor heated by a current [4] is
(12)
With the integral approximated by with , the residual is
(13)
Every depends on every through , so is dense. However, it has plenty of structure as it is a tridiagonal matrix plus the rank-one term with . Any reasonable hand-written solver would use the ShermanβMorrison formula to solve this.66 With , tridiagonal, and , we have thatwhich requires just two cheap tridiagonal solves, and . The augmented system finds the same structure without being told, since the tape contains the chain of sums that computes . A Newton step is then77 For the sake of simplicity, we use the plain \ factorizes from scratch in every step here. A KLU that reused the symbolic factorization was recorded around 5 ms for .
A = [L -Mt; P spzeros(m, n)]
Ξ = (A \ [zeros(d); F])[d+1:end]TableΒ 1: Time per Newton step for the heating problem, including tracing the program, building , and solving with it by \ (best of three runs).
| Step | ||
|---|---|---|
| 500 | 16 995 | 4.2 ms |
| 1000 | 33 995 | 8.1 ms |
| 2000 | 67 995 | 19 ms |
| 4000 | 135 995 | 46 ms |
| 8000 | 271 995 | 0.14 s |
ExampleΒ 4: The heating problem in JAX
Example 4 (The heating problem in JAX)Β Β JAX, like other standard automatic differentiation software, not record and . jax.linearize(f, x) evaluates and its partial derivatives once and returns a linearizer, the map as a program (a jaxpr). The program never forms rather it uses the unit lower triangular structure of . Since is strictly lower triangular, each row needs only the rows before it,
(14)
so computing the rows in order gives in one sweep. Each step applies row of and of at once, so and are fused into a single program. It can be transposed for reverse mode, , but not taken apart into the matrices that the augmented system needs. A Newton step for the heating problem therefore has to form the dense , with Jacobian-vector products (jax.jacfwd) or vector-Jacobian products (jax.jacrev), and factorize it densely.88 The same holds for the backends of DifferentiationInterface.jl, whose sparse mode does not help when is dense; SectionΒ 3 shows how lifting the program makes it help.
TableΒ 2: Time per Newton step for the heating problem in JAX, in double precision, including forming and solving with it by jnp.linalg.solve (best of three runs). The preparation is the first call, which compiles the step.
jax.jacfwd | jax.jacrev | |||
|---|---|---|---|---|
| Prepare | Step | Prepare | Step | |
| 500 | 70 ms | 1.2 ms | 71 ms | 1.2 ms |
| 1000 | 72 ms | 4.5 ms | 72 ms | 4.0 ms |
| 2000 | 0.10 s | 34 ms | 98 ms | 33 ms |
| 4000 | 0.29 s | 0.21 s | 0.28 s | 0.21 s |
| 8000 | 1.7 s | 1.4 s | 1.9 s | 1.5 s |
The mode makes no difference, since is square. Due to being dense things scales poorly and at things takes around s.
3. Lifting
This could seem like the end for using standard AD software for getting the sparse augmented Jacobian, however that is not true. A simple technique called Lifting [5] instead rewrites the program, so that the intermediate variables become unknowns. That is, the lifted program returns both and , and the forward relation and the outputs become equations. Said differently: The lifted program has a Jacobian that is exactly the augmented matrix of (11) 99 holds only at , the values the program computes; for other , the first block measures how far is from satisfying the forward relation.
(15)
Now the Jacobian of is sparse, even when is dense. That means that tools such that DifferentiationInterface.jl or adsex can be used to extract the sparse Jacobian. By hand, lifting every operation would mean rewriting the whole program as equations, so in practice only the few intermediate variables that couple many others are lifted, such as the integral in the heating problem.
ExampleΒ 5: Lifting the heating problem by hand
Example 5 (Lifting the heating problem by hand)Β Β We lift the partial sums of the integral, with , rather than only the total, whose equation would read every input, and the outputs , where is the heating residual of Example 3 with . The lifted variables are , the outputs are picked out by , and the lifted program is
(16)
where is the bidiagonal matrix of Example 1 and . The first block row is the cumulative sum of Example 1, the second defines the outputs, and the third sets them to zero, so the Jacobian has exactly the form of in (11).1010 The last block row says , so can be eliminated, which leaves with fewer unknowns. The scripts and TableΒ 3 use this smaller form.
TableΒ 3: Newton steps for the lifted heating problem. The preparation detects the pattern of and groups its columns, and for asdex also compiles the Jacobian; it is timed once. A step computes and solves with it, with \ for DifferentiationInterface.jl and with SciPyβs sparse LU for asdex, since JAX has no sparse direct solver; best of three runs.
| DifferentiationInterface.jl | asdex | ||||
|---|---|---|---|---|---|
| Prepare | Step | Prepare | Step | ||
| 500 | 3 497 | 0.22 ms | 0.34 ms | 57 ms | 0.79 ms |
| 1000 | 6 997 | 0.41 ms | 0.64 ms | 38 ms | 0.96 ms |
| 2000 | 13 997 | 0.87 ms | 1.3 ms | 77 ms | 4.2 ms |
| 4000 | 27 997 | 2.3 ms | 3.1 ms | 0.12 s | 3.1 ms |
| 8000 | 55 997 | 24 ms | 9.7 ms | 0.25 s | 4.0 ms |
ExampleΒ 6: Estimating the heating parameter
Example 6 (Estimating the heating parameter)Β Β The same idea regarding lifting can be used in mathematical programming software like JuMP.jl. In this example try to estimate based on a set of noisy temperature measurements, using the UnoSolver, that is solving the following nonlinear program
(17)
In JuMP.jl, the sum can be written in two ways. Directly defining s as an expression
s = @expression(model, sum(exp(-x[j]) for j in 1:n))and lifted, it is a variable with a constraint of its own,
@variable(model, s, start = n) # sum(exp(-x[j])) = n at the start x = 0
@constraint(model, s == sum(exp(-x[j]) for j in 1:n))The expression is not a fixed number but a shorthand, which JuMP.jl substitutes into every constraint that uses s. As a variable, is an unknown of its own, which Uno updates along with , and its constraint makes it equal the sum only at the solution. With the expression, every constraint reads every , so the Jacobian and the Hessian that JuMP.jl hands to Uno become dense. Lifted, only the constraint of reads all of , and since JuMP.jl differentiates each constraint on its own, that one full row is cheap and both matrices are sparse.
TableΒ 4: Solving the estimation with lifted and as an expression, with Uno through JuMP.jl; both estimate . The memory is that allocated in Julia.
| Lifted | Expression | |||
|---|---|---|---|---|
| Time | Memory | Time | Memory | |
| 100 | 5 ms | 2.6 MiB | 3.0 s | 809 MiB |
| 200 | 28 ms | 5.2 MiB | 10.6 s | 4.2 GiB |
| 1000 | 36 ms | 25 MiB | β | β |
| 2000 | 140 ms | 50 MiB | β | β |
| 4000 | 192 ms | 102 MiB | β | β |
The script is heating_lifted.jl.
4. Summary
Every Jacobian of a program is the product . Even when the three matrices are sparse, the inverse can be dense, resulting in a dense Jacobian. Sparse differentiation pays off when is sparse, but not when it is dense. In this case actually knowing the structure of the Jacobian can be useful. Either directly knowing , , and gives us a sparse representation of the Jacobian, or lifting the program to expose intermediate variables gives a sparse Jacobian which can be connected to the original dense Jacobian through a Schur complement.1111 Eliminating the intermediate variables from the lifted Jacobian leaves the Schur complement of the upper-left block in , which is .
Cite this post
@misc{paltorp2026sparsejacobians, author = {Paltorp, Mikkel}, title = {{When the Jacobian Is Dense but the Program Is Not}}, year = {2026}, month = oct, url = {https://mipals.github.io/pdf/2026/10-23-sparse-jacobians/sparse-jacobians.pdf}, note = {Blog post}}Bibliography
- [1] A. WΓ€chter and L. T. Biegler, βOn the Implementation of an Interior-Point Filter Line-Search Algorithm for Large-Scale Nonlinear Programming,β Mathematical Programming, vol. 106, no. 1, pp. 25β57, 2006, doi: 10.1007/s10107-004-0559-y.
- [2] C. Vanaret and S. Leyffer, βImplementing a Unified Solver for Nonlinearly Constrained Optimization,β Mathematical Programming Computation, 2026, doi: 10.1007/s12532-026-00310-9.
- [3] A. Hill, G. Dalle, and A. Montoison, βAn Illustrated Guide to Automatic Sparse Differentiation.β [Online]. Available: https://iclr-blogposts.github.io/2025/blog/sparse-autodiff/
- [4] A. A. Lacey, βThermal Runaway in a Non-Local Problem Modelling Ohmic Heating. Part I: Model Derivation and Some Special Cases,β European Journal of Applied Mathematics, vol. 6, no. 2, pp. 127β144, 1995.
- [5] J. Albersmeyer and M. Diehl, βThe Lifted Newton Method and Its Application in Optimization,β SIAM Journal on Optimization, vol. 20, no. 3, pp. 1655β1684, 2010.