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 𝐹(π‘₯)=0, 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π‘€βŠ€,

(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: 𝐽𝑣=𝑃(πΏβˆ’1(π‘€βŠ€π‘£)) 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 πΏβˆ’1 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, π‘“π‘˜(π‘₯)=π‘π‘˜βˆ‘π‘—β‰€π‘˜exp(βˆ’π‘₯𝑗) with constants π‘π‘˜, computed by π‘’π‘˜=π‘’π‘˜βˆ’1+exp(βˆ’π‘₯π‘˜), 𝐿 is lower bidiagonal, and π‘€βŠ€=βˆ’diag(π‘š) with π‘šπ‘—=exp(βˆ’π‘₯𝑗) and 𝑃=diag(𝑝) are diagonal. The inverse of 𝐿 is dense,

[1000βˆ’11000βˆ’11000βˆ’11]βˆ’1=[1000110011101111].

(2)

and so is the Jacobian 𝐽=π‘ƒπΏβˆ’1π‘€βŠ€. 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,

𝑦=π½π‘£βŸΊ[πΏβˆ’π‘€βŠ€π‘ƒ0][𝑧𝑣]=[0𝑦],

(3)

whose first block row gives 𝑧=πΏβˆ’1π‘€βŠ€π‘£ 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 𝑒𝑖=Φ𝑖(𝑒1,…,π‘’π‘–βˆ’1,π‘₯) 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 d𝑒=πœ•Ξ¦πœ•π‘’d𝑒+πœ•Ξ¦πœ•π‘₯dπ‘₯, that is, 𝐿d𝑒=π‘€βŠ€dπ‘₯ with

𝐿=πΌβˆ’πœ•Ξ¦πœ•π‘’βˆˆβ„π‘‘Γ—π‘‘,π‘€βŠ€=πœ•Ξ¦πœ•π‘₯βˆˆβ„π‘‘Γ—π‘›,

(4)

and therefore 𝐽=π‘ƒπΏβˆ’1π‘€βŠ€. 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 𝑓(π‘₯1,π‘₯2)=(π‘₯1π‘₯2+sin(π‘₯1),exp(π‘₯2)), a program introduces one intermediate variable per elementary operation, four in total: 𝑒1=π‘₯1π‘₯2, 𝑒2=sin(π‘₯1), 𝑒3=𝑒1+𝑒2, and 𝑒4=exp(π‘₯2). The forward relation and the outputs are

[𝑒1𝑒2𝑒3𝑒4]βŸπ‘’=[π‘₯1π‘₯2sin(π‘₯1)𝑒1+𝑒2exp(π‘₯2)]⏟Φ(𝑒,π‘₯),𝑓(π‘₯)=[𝑒3𝑒4]=[00100001]βŸπ‘ƒ[𝑒1𝑒2𝑒3𝑒4]βŸπ‘’.

(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

πœ•Ξ¦πœ•π‘’=[0000000011000000],π‘€βŠ€=πœ•Ξ¦πœ•π‘₯=[π‘₯2π‘₯1cos(π‘₯1)0000exp(π‘₯2)],

(6)

The intermediate variables depend on π‘₯ through the forward relation itself, 𝑒(π‘₯)=Ξ¦(𝑒(π‘₯),π‘₯), so the chain rule gives

d𝑒dπ‘₯=πœ•Ξ¦πœ•π‘’d𝑒dπ‘₯+πœ•Ξ¦πœ•π‘₯⟺(πΌβˆ’πœ•Ξ¦πœ•π‘’)⏟𝐿d𝑒dπ‘₯=πœ•Ξ¦πœ•π‘₯βŸπ‘€βŠ€.

(7)

In our example the unit lower triangular matrix 𝐿 is44 Writing 𝐿=πΌβˆ’πΏΜ‚ with 𝐿̂=πœ•Ξ¦/πœ•π‘’ strictly lower triangular, 𝐿̂ is nilpotent, 𝐿̂𝑑=0, so the Neumann series of πΏβˆ’1 terminates, πΏβˆ’1=𝐼+𝐿̂+…+πΏΜ‚π‘‘βˆ’1. Here 𝐿̂2=0, since 𝑒3 depends only on intermediate variables that depend on no others, so πΏβˆ’1=𝐼+𝐿̂.

𝐿=[10000100βˆ’1βˆ’1100001],πΏβˆ’1=[1000010011100001].

(8)

Solving with 𝐿 one intermediate variable at a time, by forward substitution, gives

d𝑒dπ‘₯=πΏβˆ’1π‘€βŠ€=[π‘₯2π‘₯1cos(π‘₯1)0π‘₯2+cos(π‘₯1)π‘₯10exp(π‘₯2)].

(9)

The Jacobian of 𝑓(π‘₯)=𝑃𝑒(π‘₯) picks out the rows of the outputs,

𝐽=𝑃d𝑒dπ‘₯=π‘ƒπΏβˆ’1π‘€βŠ€=[π‘₯2+cos(π‘₯1)π‘₯10exp(π‘₯2)].

(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)Β Β 

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 partial
3
# derivatives, the entries L[i, j] = -βˆ‚Ξ¦α΅’/βˆ‚uβ±Ό and Mα΅€[i, k] = βˆ‚Ξ¦α΅’/βˆ‚xβ‚–.
4
using LinearAlgebra, SparseArrays
5
6
# The tape: rows, columns and values of the entries of L - I and Mα΅€,
7
# and the number of nodes
8
const L_entries = (Int[], Int[], Float64[])
9
const Mt_entries = (Int[], Int[], Float64[])
10
const nodes = Ref(0)
11
12
# index > 0 is the node uα΅’, index < 0 is the input xβ‚‹α΅’
13
struct Tracked
14
value::Float64
15
index::Int
16
end
17
18
# Append the node with the given value and (argument => partial) pairs
19
function record!(value, args...)
20
i = nodes[] += 1
21
for (a, βˆ‚a) in args
22
rows, cols, vals = a.index > 0 ? L_entries : Mt_entries
23
push!(rows, i)
24
push!(cols, abs(a.index))
25
push!(vals, a.index > 0 ? -βˆ‚a : βˆ‚a)
26
end
27
return Tracked(value, i)
28
end
29
30
Base.:+(a::Tracked, b::Tracked) = record!(a.value + b.value, a => 1.0, b => 1.0)
31
Base.:-(a::Tracked, b::Tracked) = record!(a.value - b.value, a => 1.0, b => -1.0)
32
Base.:*(a::Tracked, b::Tracked) = record!(a.value * b.value, a => b.value, b => a.value)
33
Base.:/(a::Tracked, b::Tracked) =
34
record!(a.value / b.value, a => 1 / b.value, b => -a.value / b.value^2)
35
Base.:+(a::Tracked, c::Real) = record!(a.value + c, a => 1.0)
36
Base.:+(c::Real, a::Tracked) = a + c
37
Base.:-(a::Tracked, c::Real) = a + (-c)
38
Base.:-(c::Real, a::Tracked) = record!(c - a.value, a => -1.0)
39
Base.:-(a::Tracked) = record!(-a.value, a => -1.0)
40
Base.:*(a::Tracked, c::Real) = record!(a.value * c, a => Float64(c))
41
Base.:*(c::Real, a::Tracked) = a * c
42
Base.:/(a::Tracked, c::Real) = a * inv(c)
43
Base.:/(c::Real, a::Tracked) = record!(c / a.value, a => -c / a.value^2)
44
Base.exp(a::Tracked) = (v = exp(a.value); record!(v, a => v))
45
Base.sin(a::Tracked) = record!(sin(a.value), a => cos(a.value))
46
Base.:^(a::Tracked, p::Integer) = record!(a.value^p, a => p * a.value^(p - 1))
47
48
# Run f on tracked inputs and return f(x) with the sparse matrices
49
# L = I - βˆ‚Ξ¦/βˆ‚u, Mα΅€ = βˆ‚Ξ¦/βˆ‚x and the selection P of the outputs, so
50
# that the Jacobian of f at x is P L⁻¹ Mα΅€.
51
function trace(f, x)
52
foreach(empty!, (L_entries..., Mt_entries...))
53
nodes[] = 0
54
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 * x
57
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, P
60
end

With the matrices at hand, the Jacobian itself is a triangular solve with a sparse right-hand side, 𝐽=𝑃(πΏβˆ’1π‘€βŠ€). Column π‘˜ of πΏβˆ’1π‘€βŠ€ 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)Β Β 

julia
1
using SparseSparse # Required for \ with a sparse right-hand side
2
include("tape.jl")
3
4
# u''(xα΅’) by central differences, with u = 0 at both ends
5
function cdiff2(x, i, h)
6
left = i > 1 ? x[i-1] : 0.0
7
right = i < length(x) ? x[i+1] : 0.0
8
return (left - 2x[i] + right) / h^2
9
end
10
11
# Bratu, u'' + Ξ» exp(u) = 0: each output reads three inputs
12
function bratu(x; Ξ»=1.0)
13
h = 1 / (length(x) + 1)
14
return [cdiff2(x, i, h) + Ξ» * exp(x[i]) for i in eachindex(x)]
15
end
16
17
# Nonlocal Ohmic heating, u'' + Ξ» exp(-u) / (βˆ«β‚€ΒΉ exp(-u) dx)Β² = 0, of the
18
# next section: every output reads the sum s, so every input
19
function heating(x; Ξ»=2.0)
20
h = 1 / (length(x) + 1)
21
s = sum(exp(-xj) for xj in x) # h * s is the integral
22
scaling = Ξ» / (h * s)^2
23
return [cdiff2(x, i, h) + scaling * exp(-x[i]) for i in eachindex(x)]
24
end
25
26
x = 0.1 .* rand(1000)
27
_, L, Mt, P = trace(bratu, x) # nnz(L) = 13000, nnz(M) = 3998
28
J = P * (L \ Mt) # tridiagonal: nnz(J) = 2998
29
_, L, Mt, P = trace(heating, x) # nnz(L) = 27997, nnz(M) = 4998
30
J = P * (L \ Mt) # dense: nnz(J) = 1000000

2. 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 𝐽𝑣=𝑃(πΏβˆ’1(π‘€βŠ€π‘£)) costs one forward substitution. Newton’s method for 𝐹(π‘₯)=0, however, needs a solve, 𝐽Δ=𝐹(π‘₯), in every step, and with a dense 𝐽 that means forming 𝐽 and factorizing it, at a cost of order 𝑛3. The cumulative sum also hinted at the way around this: given 𝑃, 𝐿, and 𝑀, the solve can be written as one sparse augmented system,

𝐽Δ=𝐹(π‘₯)⟺[πΏβˆ’π‘€βŠ€π‘ƒ0]⏟𝐴[𝑧Δ]=[0𝐹(π‘₯)],

(11)

whose first block row gives 𝑧=πΏβˆ’1π‘€βŠ€Ξ” 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 [πΏβŠ€π‘ƒβŠ€βˆ’π‘€0][πœ†π‘€]=[0𝑔], the system with 𝐴⊀.

ExampleΒ 3: Ohmic heating

Example 3 (Ohmic heating)Β Β  A nonlocal model of a resistor heated by a current [4] is

𝑒″+πœ†exp(βˆ’π‘’)(∫01exp(βˆ’π‘’)dπ‘₯)2=0,𝑒(0)=𝑒(1)=0.

(12)

With the integral approximated by β„Žπ‘  with 𝑠=βˆ‘π‘—exp(βˆ’π‘₯𝑗), the residual is

𝐹𝑖(π‘₯)=π‘₯π‘–βˆ’1βˆ’2π‘₯𝑖+π‘₯𝑖+1β„Ž2+πœ†exp(βˆ’π‘₯𝑖)(β„Žπ‘ )2.

(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 2πœ†β„Ž2𝑠3π‘’π‘’βŠ€ with 𝑒𝑖=exp(βˆ’π‘₯𝑖). Any reasonable hand-written solver would use the Sherman–Morrison formula to solve this.66 With 𝐽=𝑇+π›Ύπ‘’π‘’βŠ€, 𝑇 tridiagonal, and 𝛾=2πœ†/(β„Ž2𝑠3), we have thatπ½βˆ’1𝑏=π‘‡βˆ’1π‘βˆ’π›Ύπ‘’βŠ€π‘‡βˆ’1𝑏1+π›Ύπ‘’βŠ€π‘‡βˆ’1π‘’π‘‡βˆ’1𝑒,which requires just two cheap tridiagonal solves, π‘‡βˆ’1𝑏 and π‘‡βˆ’1𝑒. 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 𝑛=8000.

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).

𝑛nnz(𝐴)Step
50016 9954.2 ms
100033 9958.1 ms
200067 99519 ms
4000135 99546 ms
8000271 9950.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 𝑣↦𝑃(πΏβˆ’1(π‘€βŠ€π‘£)) as a program (a jaxpr). The program never forms πΏβˆ’1 rather it uses the unit lower triangular structure of 𝐿=πΌβˆ’πΏΜ‚. Since 𝐿̂ is strictly lower triangular, each row needs only the rows before it,

𝑑𝑖=(π‘€βŠ€π‘£)𝑖+βˆ‘π‘—<𝑖𝐿̂𝑖𝑗𝑑𝑗,𝑖=1,…,𝑑,

(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.jacfwdjax.jacrev
PrepareStepPrepareStep
50070 ms1.2 ms71 ms1.2 ms
100072 ms4.5 ms72 ms4.0 ms
20000.10 s34 ms98 ms33 ms
40000.29 s0.21 s0.28 s0.21 s
80001.7 s1.4 s1.9 s1.5 s

The mode makes no difference, since 𝐽 is square. Due to 𝐽 being dense things scales poorly and at 𝑛=16000 things takes around 17s.

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 𝐺(𝑒,π‘₯)=(0,𝑓(π‘₯)) holds only at 𝑒=𝑒(π‘₯), the values the program computes; for other 𝑒, the first block measures how far 𝑒 is from satisfying the forward relation.

𝐺(𝑒,π‘₯)=[𝑒𝑃𝑒]βˆ’[Ξ¦(𝑒,π‘₯)0],𝐺(𝑒(π‘₯),π‘₯)=[0𝑓(π‘₯)],πœ•πΊπœ•(𝑒,π‘₯)=[πΌβˆ’πœ•Ξ¦πœ•π‘’βˆ’πœ•Ξ¦πœ•π‘₯𝑃0]=[πΏβˆ’π‘€βŠ€π‘ƒ0],

(15)

Now the Jacobian of 𝐺 is sparse, even when πΏβˆ’1 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, π‘ π‘˜=π‘ π‘˜βˆ’1+exp(βˆ’π‘₯π‘˜) with 𝑠0=0, 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 𝑃=[0𝐼], and the lifted program is

𝐺(𝑠,π‘Ÿ,π‘₯)=[πΏπ‘ βˆ’π‘’(π‘₯)π‘Ÿβˆ’πΉ(π‘₯,𝑠𝑛)π‘Ÿ],πœ•πΊπœ•(𝑠,π‘Ÿ,π‘₯)=[𝐿0diag(𝑒)βˆ’πœ•πΉπœ•π‘ πΌβˆ’πœ•πΉπœ•π‘₯0𝐼0],

(16)

where 𝐿 is the bidiagonal matrix of Example 1 and π‘’π‘˜=exp(βˆ’π‘₯π‘˜). 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 π‘Ÿ=0, 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.

𝑛nnz(πœ•πΊ)DifferentiationInterface.jlasdex
PrepareStepPrepareStep
5003 4970.22 ms0.34 ms57 ms0.79 ms
10006 9970.41 ms0.64 ms38 ms0.96 ms
200013 9970.87 ms1.3 ms77 ms4.2 ms
400027 9972.3 ms3.1 ms0.12 s3.1 ms
800055 99724 ms9.7 ms0.25 s4.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

minπ‘₯,πœ†β‰₯0βˆ‘π‘–=1𝑛(π‘₯π‘–βˆ’π‘¦π‘–)2subject toπ‘₯π‘–βˆ’1βˆ’2π‘₯𝑖+π‘₯𝑖+1+πœ†exp(βˆ’π‘₯𝑖)𝑠2=0,𝑖=1,…,𝑛.

(17)

In JuMP.jl, the sum 𝑠=βˆ‘π‘—exp(βˆ’π‘₯𝑗) 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 πœ†β‰ˆ2.00. The memory is that allocated in Julia.

𝑛LiftedExpression
TimeMemoryTimeMemory
1005 ms2.6 MiB3.0 s809 MiB
20028 ms5.2 MiB10.6 s4.2 GiB
100036 ms25 MiBβ€”β€”
2000140 ms50 MiBβ€”β€”
4000192 ms102 MiBβ€”β€”
FigureΒ 1: The estimation for 𝑛=50 with the true πœ†=2. The data have noise with standard deviation 0.03 added.

The script is heating_lifted.jl.

4. Summary

Every Jacobian of a program is the product 𝐽=π‘ƒπΏβˆ’1π‘€βŠ€. Even when the three matrices are sparse, the inverse πΏβˆ’1 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 [πΏβˆ’π‘€βŠ€π‘ƒ0], which is 0βˆ’π‘ƒπΏβˆ’1(βˆ’π‘€βŠ€)=π‘ƒπΏβˆ’1π‘€βŠ€=𝐽.

Cite this post

bibtex
@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.