For a scalar-valued function of several variables, the gradient at a point is a vector that points in the direction of steepest increase. Its magnitude is the maximum rate of increase at that point. Taking the gradient at every point gives a gradient vector field. At any particular point, one vector from that field describes the local direction and rate.
For a two-dimensional matrix X and a scalar loss L, every element Xij can affect the loss, so ∂L/∂X contains one partial derivative for each matrix element. Conceptually, we can flatten the independent scalar entries of X into a vector. By definition, their derivatives form an ordinary gradient vector. The matrix-shaped gradient is a view of that vector, described by a shape and strides over its storage, and does not need to be contiguous.
Backpropagation therefore starts from a scalar objective, usually the loss. A vector-valued output has a Jacobian, not one gradient vector, and the vector itself does not define a unique meaning of «better». We first need a preference that combines its components into a scalar.
That scalar objective can encode the preference as a weighted combination. During backpropagation, its upstream gradient supplies those weights, reducing the output Jacobian to the single gradient direction relevant to the loss.
Every entry of JcaJab is therefore equal to the corresponding entry of Jcb.
Now consider a sequence of dependencies
a→b→c→d→l
The Jacobian from a to l is the product of the Jacobians along the path. Applying the same rule to the rightmost pair after each equals sign gives
JldJdcJcbJba=JldJdcJca=JldJda=Jla
If l is scalar and x∈Rn, the final Jacobian Jlx has shape 1×n. Its entries are ∂xi∂l, so this row vector contains the gradient information for x. Under the column-gradient convention,
∇xl=JlxT
If x is a parameter vector, gradient descent updates it as
x←x−η∇xl
Computing the gradient this way requires materializing every intermediate Jacobian, which can be a huge matrix.
These Jacobians are often sparse because one input usually affects only part of the output, leaving most entries equal to zero.
Consider a dependency chain a→b→c→d→l, where l is a scalar. Suppose we already know Jld. This is the upstream gradient. Because l is scalar, Jld contains exactly one derivative for each element of d. Mathematically it has shape 1×∣d∣ when d is flattened, but in code those entries are stored or viewed with the same tensor shape as d. How do we compute Jlc?
Jlc=JldJdc
There are two approaches.
Materialize Jdc and multiply Jld by it. This is memory-heavy because Jdc can be huge and mostly sparse.
Compute the vector-Jacobian product directly, without forming Jdc.
Matrix multiplication can be decomposed into scalar multiplications and additions, often executed as fused multiply-add operations. Therefore, if we know how to backpropagate through scalar multiplication and addition, we can also backpropagate through matrix multiplication.
Doing this one scalar at a time would be extremely inefficient. Computing each output element would require traversing the graph and making many function calls, creating far more overhead than useful work. A much better approach is to represent the entire matrix multiplication as a single operation and execute it with a CUDA kernel built using CUTLASS. We can do exactly that while preserving automatic differentiation.
A general way to derive the backward pass for any operation is to start with every possible path from an input to the loss. Most of those paths usually have zero influence, so the full expression quickly collapses into something simple.
Consider a matrix multiplication
Y=XW
Let the upstream gradient be G=∂Y∂L, with the same shape as Y. For one element Xab, the chain rule initially sums over every element of Y
∂Xab∂L=i∑j∑∂Xab∂Yij∂Yij∂L
Now substitute Yij=∑kXikWkj into the full expression
∂Xab∂L=i∑j∑∂Xab∂(k∑XikWkj)∂Yij∂L
First, move the derivative inside the sum over k
∂Xab∂L=i∑j∑(k∑∂Xab∂(XikWkj))∂Yij∂L
Because W does not depend on X, we can move it outside the derivative
∂Xab∂L=i∑j∑k∑Wkj∂Xab∂Xik∂Yij∂L
Next, split the sum over i into the case i=a and all cases where i=a
Every derivative in the second term is zero because Xak and Xab are different elements when k=b. Only the left term remains
∂Xab∂L=j∑Wbj∂Xab∂Xab∂Yaj∂L
Finally, ∂Xab∂Xab=1, so
∂Xab∂L=j∑Wbj∂Yaj∂L
This is already very close to matrix multiplication. To produce element (a,b), we need the dot product of row a and column b. Move the upstream gradient to the left, then rewrite Wbj as (WT)jb
∂Xab∂L=j∑∂Yaj∂L(WT)jb
Now the indices have exactly the form of matrix multiplication. We take row a of ∂Y∂L and column b of WT. Therefore
∂X∂L=GWT
We can therefore compute the gradient with a fast CUDA GEMM instead of traversing a graph of scalar multiplications and additions.
Gradient with Respect to the Weights
The gradient with respect to the weights follows exactly the same process. Start by summing every possible effect of Wab on the output
∂Wab∂L=i∑j∑∂Wab∂Yij∂Yij∂L
Substitute Yij=∑kXikWkj into the full expression
∂Wab∂L=i∑j∑∂Wab∂(k∑XikWkj)∂Yij∂L
Move the derivative inside the sum over k
∂Wab∂L=i∑j∑(k∑∂Wab∂(XikWkj))∂Yij∂L
Because X does not depend on W, we can move it outside the derivative
∂Wab∂L=i∑j∑k∑Xik∂Wab∂Wkj∂Yij∂L
First, split the sum over j into the case j=b and all cases where j=b
Every derivative in the second term is zero because Wkb and Wab are different elements when k=a. Only the left term remains
∂Wab∂L=i∑Xia∂Wab∂Wab∂Yib∂L
Finally, ∂Wab∂Wab=1, so
∂Wab∂L=i∑Xia∂Yib∂L
Again, this is close to matrix multiplication. Rewrite Xia as (XT)ai
∂Wab∂L=i∑(XT)ai∂Yib∂L
This is row a of XT multiplied by column b of the upstream gradient. Therefore
∂W∂L=XTGGradient of ReLU
ReLU is elementwise, so its backward rule is the same for a vector, matrix, or N-dimensional tensor. Conceptually, flatten the logical elements into a vector and apply the scalar rule independently to each element
yi=max(0,xi)∂xi∂L={∂yi∂L,0,xi>0xi≤0
At xi=0, the implementation chooses the conventional subgradient 0. The result can then be viewed with the original shape. When the storage is contiguous, the implementation can process it as a flat contiguous buffer. Other tensor views may require respecting their strides.
Each operation has a forward pass and a backward pass. The forward pass computes output tensors from input tensors and remembers the values needed to compute the local derivatives later.
Backpropagation starts from a scalar loss, whose derivative with respect to itself is 1.
The backward pass topologically sorts the computation graph and traverses it in reverse, from the result through the operations that produced it and toward the input tensors. Each operation consumes its accumulated upstream gradient, computes contributions to the gradients of its inputs, and accumulates those contributions.
Reverse topological order ensures that an operation is processed only after all of its downstream consumers have contributed. Its upstream gradient is therefore complete before the backward pass moves further toward the inputs.