Batched Backpropagation as a Linear Solve
This post extends Backpropagation as a Linear Solve to batches of inputs. The main change is that the (sparse) matrices of scalars from the previous post become (sparse) matrices whose entries are linear maps.11 More often than not the matrices are sparse—which is key for the efficiency of automatic differentiation. To readers used to the usual presentation of backpropagation as a pass over a computational graph, matrices may seem an odd choice. The two views are the same: the sparsity pattern below the diagonal of a lower triangular matrix is a Directed Acyclic Graph (DAG).
The post ends with a Julia implementation of a neural network whose gradient is , the same formula as before, written as a linear solve in the style of Edelman, Akyürek, and Wang [1]. The inverse is never formed: because is unit lower triangular, is unit upper triangular, and its inverse can be applied efficiently using back substitution.
1. The batched forward pass
The previous post considered a single input and a loss of its outputs. However, the loss function is often based on minimizing a data-fitting term of a minibatch of input-output pairs (). Due to the linearity of a sum the full gradient over the input-output data could be computed by simply summing the gradient contribution for each example. In practice, it is often more efficient to process the data together in a batch.22 Batching turns matrix–vector products into matrix–matrix products, which use modern hardware far better. Moreover, because the weights are shared by all the data points, they only have to be loaded into memory once for a batch, whereas processing each example separately would load them once per example. The first step is to introduce the batched input and output matrices by stacking the data points as columns, i.e.
(1)
The only difference between this and the single input case is that the application of layer now generates outputs. Writing the outputs with a capital , as for the input data, layer gives
(2)
We denote the collection of all the states by and the collection of all the layer maps by . One can think of them as vectors whose entries are the states and the layer maps, respectively, so that
(3)
However, this does not have to be the case, and the only thing we require is that is an element of a vector space with an inner product. Similarly, the parameters live in a vector space . The next section makes this precise.33 See Wikipedia for what is meant by a vector space.
2. Generalization to vector spaces
It is standard to equip the space that each layer output lives in with the Frobenius inner product. This means that for the inner product is a sum of Frobenius inner products over the layers, i.e.
(4)
The construction of the parameter space is slightly more complicated, as parameters can be shared between layers.44 For example, in a recurrent neural network. The parameters can still be thought of as a vector whose entries are parameter blocks, that is, . Layer uses only some of the blocks, and several layers may use the same block. A linear selection map picks out the blocks of layer , so that depends on only through . In a plain feed-forward network every layer has its own weight block and bias block , and . Similar to , we equip with the Frobenius inner product block by block, so that for it follows that
(5)
Lastly, we also aim to equip the output space with an inner product. We view elements in as a list of outputs, , where again the inner product is a sum of Frobenius inner products over the list,
(6)
We then define a linear output map such that it maps the full state to a corresponding output. That is and we sometimes simply write .55 Usually picks out the last layer, . It can also select several layers, or combine them, for example by averaging.
2.1. The loss
We often describe the loss function as a function of the parameters only, but it is more natural to view it as a function of the states and the parameters. That is, , where usually
(7)
where is a data-fitting term that compares the outputs with the targets , and is a regularization term. The derivation below only needs and to be differentiable, which also makes differentiable. Only the gradients and of enter, as the right-hand side of the adjoint equation and as the last term of the gradient.
Example 1: Squared loss
Example 1 (Squared loss) The natural extension of the scalar loss is the squared Frobenius norm of the residuals, written with the inner product on ,66 The scalar loss comes from the inner product together with a regularization of the parameters,
(8)
This is the loss of the examples and the code of this post.
2.2. Adjoints
For a linear map between two of these spaces, the adjoint is defined by
(9)
Moving an operator across an inner product therefore changes the inner product from the operator’s output space to its input space.
Example 2: Adjoint of the selection map
Example 2 (Adjoint of the selection map) Take layers, each producing a matrix with columns, one per data point, and let the loss depend on the first and the last layer output. Then
(10)
where and have the sizes of and . The selection map is . We are free to select other layers, or the same layer more than once, as long as each selected has the size of the corresponding entry of . For any and , writing out the inner product on and adding a zero term for the unselected layer gives
(11)
Hence the adjoint places the selected blocks back in their original positions, with zeros elsewhere, i.e. . If a layer is selected more than once, sums the corresponding blocks of in its position. The inner product starts in , the output space of , and ends in , its input space. In the common case where only the last layer is selected, and .
2.3. Gradients
The gradient is defined through an inner product. For a differentiable scalar function of , such as the loss , the derivative is a linear map from directions to numbers.77 denotes the dual space of , which is the space of all linear functionals on . This is a technicality that we revisit in Which Way Is Down? First-Order Optimizers as a Choice of Norm. The Riesz representation theorem88 In a finite-dimensional inner product space every linear functional has the form for exactly one , its Riesz representative. says that such a map is an inner product with a fixed element of . That element is the gradient
(12)
So to find a gradient, we write the derivative in the form and read off the other argument. The same definition applies on and , each with its own inner product. Changing the inner product therefore changes the gradient, but not the derivative.
Example 3: Gradients of the loss
Example 3 (Gradients of the loss) A state change changes the outputs by , since is linear. Moving across the inner product with its adjoint gives
(13)
with evaluated at . For the squared loss, ,99 By the product rule and the symmetry of the inner product, [2]. and gives .
The last step depends on the inner product on . With instead, for a self-adjoint and positive definite , the same derivative reads
(14)
so the gradient becomes . In the same way, the gradient of becomes the preconditioned , which Which Way Is Down? builds optimizers from.
3. The batched gradient
Note that the parameters enter the loss function twice: directly as , and through the states . Differentiating in a parameter direction , the chain rule gives one term for each,
(15)
In order to find the gradient , we need to write the first term in the form so that we can read off the other argument—the Riesz representative of the derivative.
3.1. The forward relation
To relate to , differentiate the forward relation in the direction . The left side gives . On the right, again enters both directly and through , and the chain rule gives
(16)
The first term accounts for changes through the states, while the second accounts for the explicit parameter dependence.1010 Both partial derivatives are evaluated at the current . In , is held fixed; in , is held fixed. The total state dependence enters through . Moving the state term to the left gives the linear equation
(17)
where and are the operator versions of and from the previous post. Solving it, , gives for one direction . This is forward-mode differentiation, and the full gradient this way would need one solve per direction in a basis of , that is, solves.
3.2. The adjoint equation
Solving (17) gives . Substituting this into the first term of (15) and moving first , which maps to itself, and then , which maps to , across the inner product using their respective adjoints gives
(18)
where . Rather than forming the inverse, we compute by solving the adjoint equation1111 For invertible , the inverse and adjoint commute:Taking adjoints of reverses the order, giving ; the reverse product is also the identity. Moving across the inner product therefore gives a solve with .
(19)
whose right-hand side does not depend on , so a single solve serves every direction. This is reverse-mode differentiation: as shown below, is lower block triangular, so is upper block triangular, and the solve runs from the last layer back to the first. Collecting the terms,
(20)
Reading off the Riesz representative of gives
(21)
3.3. The operators as block (sparse) matrices
Viewing the states and the parameters as lists, and can be written as block matrices whose entries are linear maps. For a state change , layer can only respond to the states that come strictly before it, so entry of 1212 Recall that maps state changes to state changes, so is again a list with one entry per layer. is
(22)
so block of is nonzero only if layer reads the state . In the same way, for a parameter direction , layer only sees the parameters , so by the chain rule entry of 1313 Recall that maps parameter changes to the state changes they cause directly: it takes a list over the parameter blocks to a list over the layers. Its adjoint goes the other way. is
(23)
so block of is nonzero only if selects the parameter block . The indices of are written in the order because it is the adjoint of the block of , at position , as shown below. Written out, with denoting the zero map,
(24)
Layer only reads earlier states, so is lower block triangular with identities on the diagonal. In practice a layer reads only a few of the states, and most parameter blocks are used by a single layer, so most blocks are , and and are sparse. Since the inner products on and are sums over the layers and the parameter blocks, the adjoints follow the same pattern as for matrices: transpose the block pattern and take the adjoint of each block, .1414 This relies on the inner products treating the blocks separately. An inner product that couples the blocks, such as with the Fisher information of natural gradient descent, gives an adjoint that also involves . In practice one keeps the separable inner product for the derivation and applies to the resulting gradient afterwards. This gives
(25)
Since is upper block triangular, the adjoint equation is solved by back substitution from the last layer to the first. Both this solve and the application of only require the action of the adjoint blocks on the entries .
Example 4: Affine layers
Example 4 (Affine layers) Let every layer apply an affine map to the states it reads, followed by an entrywise activation . Layer reads the state through a weight and adds a bias , so that
(26)
where the sum runs over the states that layer reads, and is the input data. The selection map picks out the weights and the bias of layer among the parameter blocks , and two layers share a block if, for example, . A plain feed-forward network only has the weights . The product rule and the entrywise linearization of give
(27)
where denotes entrywise multiplication.1515 The weights read the input data , which is fixed and not part of , so they give no block in , but they still give blocks of . Treating as a state would add the blocks and give the gradient with respect to the input. Their adjoints follow from two properties of the Frobenius inner product: entrywise multiplication is self-adjoint, , and a factor of a matrix product moves to the other side, .1616 Both equalities follow from . The first only regroups the product,and the second uses the cyclic property of the trace, Hence
(28)
4. A code example
The code is adapted from the Julia notebook Backprop with Backslash accompanying [1], which fixes a block bidiagonal and a block diagonal . Here both are general sparse matrices of operators, so that a layer can read any earlier state and share parameter blocks with other layers.
We introduce a network of layers, which produce the states from the input , and include data points. The layers are those of Example 4. Besides the chain weights , layers 3, 5, and 6 also read , , and through the skip weights , , and , and layers 3 and 4 share both their chain weight, , and their bias, . With 8 weight blocks and 5 bias blocks, .1717 Each of the 6 layers has a chain weight and a bias, but layers 3 and 4 share theirs, which leaves pairs. Each of the 3 skip connections adds a weight block of its own, so . The operator then has the pattern
(29)
Already here one can see that is sparse, with only 14 nonzero blocks out of 36, and only 8 of the 15 blocks below the diagonal. The deeper the network, the sparser it becomes, as each layer reads only a few of the earlier states.
Column of has a block in row for every parameter block that selects.1818 We number the weights first, , , , , and along the chain and , , and for the skip connections, and the biases last, so that, for example, and . This gives
(30)
The rows of the shared weight and of the shared bias each hold two blocks, so these two entries of sum the contributions of layers 3 and 4.
In the code, each block is an operator that stores the map and its adjoint, and and are sparse matrices of these operators. The only new methods are the back substitution with and the application of , which both visit only the stored blocks. The script prints the patterns of and (the transpose of the pattern of above) and compares the gradient with a central difference along a random direction.
backbackback.jl1using LinearAlgebra, Random, SparseArrays2Random.seed!(42)34# A linear map stored together with its adjoint5struct Operator6 operator7 adjoint8 sym::String9end10Base.adjoint(A::Operator) = Operator(A.adjoint, A.operator, "$(A.sym)′")11Base.:*(A::Operator, X) = A.operator(X)12Base.show(io::IO, A::Operator) = print(io, A.sym)13Base.zero(::Type{Operator}) = Operator(zero, zero, "𝒪") # For printing14const ℐ = Operator(identity, identity, "ℐ")1516# Adjoints of sparse matrices whose entries are operators.17const AdjointOperators = Adjoint{<:Any, <:SparseMatrixCSC{Operator}}1819# Back substitution with ℒ*: Λ_j = g_j - ∑_{i>j} ℒ_ij* Λ_i.20function Base.:\(A::AdjointOperators, g::Vector)21 ℒ = parent(A) # Get the non-adjoint, but apply adjoint later22 rows, blocks = rowvals(ℒ), nonzeros(ℒ) # Column j: blocks[r], rows[r]23 Λ = copy(g)24 for j in length(g):-1:1, r in nzrange(ℒ, j)25 i = rows[r]26 if i > j27 Λ[j] -= blocks[r]' * Λ[i]28 end29 end30 return Λ31end3233# ℳ = (ℳ′)*: entry k sums the contributions of the layers that use θ_k.34function Base.:*(A::AdjointOperators, Λ::Vector)35 ℳ′ = parent(A) # Get the non-adjoint, but apply adjoint later36 rows, blocks = rowvals(ℳ′), nonzeros(ℳ′)37 return [sum(blocks[r]' * Λ[rows[r]] for r in nzrange(ℳ′, k))38 for k in axes(ℳ′, 2)]39end4041# Defining the activation function and its derivative42σ(x) = exp(-x)43σ′(x) = -exp(-x)4445# Layer i reads the state U_j through the weight W_ij = θ_k for every46# j => k in W[i], and adds the bias b_i = θ_b[i]. Layers 3 and 4 share47# both their weight θ₃ and their bias θ₁₁.48d = [5, 4, 4, 4, 4, 3, 1] # Feature sizes of U₀, …, U₆49W = [[0 => 1], [1 => 2], [2 => 3, 1 => 6], [3 => 3], [4 => 4, 2 => 7],50 [5 => 5, 3 => 8]]51b = [9, 10, 11, 11, 12, 13]52N, K = 6, 13 # Layers, parameter blocks5354function network(θ)55 U, Δ = Matrix{Float64}[], Matrix{Float64}[]56 state(j) = j == 0 ? U₀ : U[j]57 for i in 1:N58 Z = sum(θ[k] * state(j) for (j, k) in W[i]) .+ θ[b[i]]59 push!(U, σ.(Z))60 push!(Δ, σ′.(Z))61 end62 return U, Δ63end6465# The sparse matrices ℒ = I - D_U Φ and ℳ′ = D_θ Φ, block by block.66function operators(θ, U, Δ)67 state(j) = j == 0 ? U₀ : U[j]68 ℒ, ℳ′ = spzeros(Operator, N, N), spzeros(Operator, N, K)69 for i in 1:N70 ℒ[i, i] = ℐ71 for (j, k) in W[i]72 Wij, Uj = θ[k], state(j)73 if j > 074 ℒ[i, j] = Operator(E -> -(Δ[i] .* (Wij * E)),75 Λ -> -(Wij' * (Δ[i] .* Λ)), "ℒ")76 end77 ℳ′[i, k] = Operator(h -> Δ[i] .* (h * Uj),78 Λ -> (Δ[i] .* Λ) * Uj', "ℳ′")79 end80 ℳ′[i, b[i]] = Operator(h -> Δ[i] .* h,81 Λ -> vec(sum(Δ[i] .* Λ, dims=2)), "ℳ′")82 end83 return ℒ, ℳ′84end8586# The data and the parameters.87m = 10 # Data points88α = 1e-2 # Weight of the regularization α/2 ⟨θ, θ⟩89init(dims...) = 0.01 * randn(dims...)90U₀, Y = init(d[1], m), init(d[end], m)91θ = Vector{Array{Float64}}(undef, K)92for i in 1:N93 for (j, k) in W[i]94 θ[k] = init(d[i+1], d[j+1])95 end96 θ[b[i]] = init(d[i+1])97end9899# The gradient of J(θ) = ‖U_N - Y‖²/(2m) + α/2 ⟨θ, θ⟩.100J(θ) = norm(network(θ)[1][N] - Y)^2 / (2m) + α / 2 * dot(θ, θ)101U, Δ = network(θ)102ℒ, ℳ′ = operators(θ, U, Δ)103∇Uℓ = [i == N ? (U[N] - Y) / m : zero(U[i]) for i in 1:N] # 𝒫*(∇f)104Λ = ℒ' \ ∇Uℓ105∇J = ℳ′' * Λ + α * θ # ℳ(Λ) + αθ106display(ℒ)107display(ℳ′)108109# Central difference of the loss along a random direction.110h = [randn(size(p)...) for p in θ]111ε = 1e-6112directional = (J(θ + ε * h) - J(θ - ε * h)) / (2ε)113versus_finite_difference = abs(directional - dot(∇J, h)) / abs(dot(∇J, h))114@show versus_finite_difference5. Conclusion
Going from a single input to a batch only changed the entries of the matrices, from scalars to linear maps. The overall structure stayed the same: is a unit lower block triangular matrix whose sparsity pattern is a DAG, and both and are most often sparse. The formula for the gradient also kept the same form, ,1919 In the previous post the gradient reads . The matrices and become the operators and , the transpose becomes the adjoint, becomes , and the regularization is part of . and can still be computed with a single back substitution and a single sparse product.
Cite this post
@misc{paltorp2026batchedbackpropagation, author = {Paltorp, Mikkel}, title = {{Batched Backpropagation as a Linear Solve}}, year = {2026}, month = oct, url = {https://mipals.github.io/pdf/2026/10-07-batched-backpropagation/batched-backpropagation.pdf}, note = {Blog post}}Bibliography
- [1] A. Edelman, E. Akyürek, and Y. Wang, “Backpropagation through Back Substitution with a Backslash,” SIAM Journal on Matrix Analysis and Applications, vol. 45, no. 1, pp. 429–449, 2024, doi: 10.1137/22M1532871.
- [2] P. Bright, A. Edelman, and S. G. Johnson, “Matrix Calculus (for Machine Learning and Beyond).” [Online]. Available: https://arxiv.org/abs/2501.14787