In reverse-mode autodiff, why is a reused tensor's gradient the sum of its consumers' gradients?
answer
- one node, several consumers
- multivariable chain rule over paths
- copy forward means add backward
- sum, never average
- final only after the last consumer
basics
~20 sA reused tensor appears in several terms of the chain rule, so its gradient is the sum of one contribution per consumer. The reverse sweep adds each contribution into a single buffer as it reaches that consumer.
solid answer
~50 sFan-out in the forward pass is fan-in in the backward pass. If `x` feeds `y1` and `y2`, the multivariable chain rule gives `dL/dx = dL/dy1 * dy1/dx + dL/dy2 * dy2/dx` — a sum over paths, one vector-Jacobian product per consumer. Reverse mode implements that literally: each node owns an adjoint buffer that starts at zero, and every consumer adds its contribution when the sweep reaches it. A node's gradient is therefore only final once *all* of its consumers have reported, which is why the sweep runs in reverse topological order. The practical case is a shared parameter: in a weight-tied language model the same matrix is the input embedding and the output projection, so its gradient is the sum of the lookup path and the projection path. Take only one and the model still trains — towards a different optimum.
code
python · 18 lines# forward: y1 = 3*x , y2 = x*x , L = y1 + y2
# expected: dL/dx = 3 + 2*x
x = 2.0
adjoint = {}
def accumulate(name, g):
adjoint[name] = adjoint.get(name, 0.0) + g
y1 = 3.0 * x
y2 = x * x
L = y1 + y2
# reverse sweep: dL/dy1 = 1.0, dL/dy2 = 1.0
accumulate("x", 1.0 * 3.0) # path through y1
accumulate("x", 1.0 * (2.0 * x)) # path through y2
print(L, adjoint["x"]) # 10.0 7.0go deeper
Be ready to say that if a value is used twice, its gradient is the sum of the two contributions, not one of them and not their average.
Expect to derive it from the multivariable chain rule and to describe the accumulator: each consumer adds into a buffer, and the node's gradient is final only after the last one reports.
Show that you can spot the silent version: a tied or shared parameter that receives only one of its paths still trains, just towards a different optimum. Say how you would verify per-path contributions.
Own the scaling consequence. Summed gradients from many heads or many uses change the effective step size on shared parameters, so head weighting and a separate learning rate for a shared trunk are design decisions, not defaults.
## The graph, and what "reuse" means A training step builds a directed acyclic graph: nodes are tensors, edges are operations, and the single scalar loss `L` sits at the sink. **Reuse** means one node has more than one outgoing edge — the same activation feeds two layers, the same parameter matrix is used by two operations, the same embedding table is read twice. That is *fan-out*. Reverse-mode autodiff computes, for every node `v`, its **adjoint** `v_bar = dL/dv`. It walks the graph backwards from the loss, and at each operation it turns the adjoint of the output into adjoints of the inputs (a vector-Jacobian product). The question is what happens at a node with several outgoing edges. ## The chain rule says sum Suppose `x` feeds two consumers, `y1 = f(x)` and `y2 = g(x)`, and `L` depends on `x` only through them. Perturb `x` by a small `dx`. Both `y1` and `y2` move, and both movements change `L`. To first order the two effects add: ``` dL/dx = (dL/dy1)(dy1/dx) + (dL/dy2)(dy2/dx) ``` In vector form each term is `J_i^T * y_i_bar`, the transposed Jacobian of that operation applied to that consumer's adjoint. With `k` consumers you get `k` terms. This is the total derivative — it is not an approximation, and it is not a convention that could have been chosen differently. So the rule is the neat duality: **copy in the forward pass, add in the backward pass**. A broadcast, an explicit copy, using a variable twice in an expression, and a parameter shared between two layers are all the same node with fan-out, and all of them accumulate. ## Why a buffer, and why reverse topological order An implementation cannot know a node's full gradient the moment it processes the first consumer. So each node holds an accumulator initialised to zero; each consumer's backward step *adds into* it. The node's own backward step — pushing gradient further upstream — may only run once every consumer has contributed, otherwise it would propagate a partial sum. Processing nodes in reverse topological order is exactly the scheduling discipline that guarantees this. If you ever hand-write a backward pass, the two bugs are (a) assigning instead of adding, so the last consumer overwrites the others, and (b) propagating before the last consumer has reported. ## Sum, not average Averaging over consumers would be a different, wrong derivative. If you want one path to count for less, scale it in the loss — for example by weighting a head's loss term — and let summation do the rest. A consequence worth internalising: a shared trunk feeding three heads receives the sum of three gradients, so its parameters see a systematically larger update than a single-head model at the same learning rate. The head loss weights and the trunk's learning rate are coupled. ## The silent bug Because accumulation is invisible when it works, dropping a path is one of the quietest failures in deep learning. The canonical case is **weight tying**: one matrix serves as the input embedding (a row lookup for the tokens in the batch — a sparse gradient) and as the output projection (a dense matmul against every row — a dense gradient). The correct gradient is the sum of the sparse and dense contributions. An implementation that keeps two copies of the matrix and only updates one, or that severs one path, produces a model that trains, converges and looks fine on the loss curve — it is simply optimising a different objective from the one you wrote down. The same shape appears in multi-head models: three heads over one trunk, and all three upstream gradients must arrive before the trunk's parameters are updated *once*. Updating the trunk three times, once per head, is not equivalent — three sequential steps of size `lr` from three different points is a different trajectory from one step along the summed direction, and the difference grows with the learning rate. ## How to check Craft a loss that depends on only one path, run one backward pass, and inspect which parameters received a nonzero gradient. Then compare the shared parameter's gradient against the sum of the two single-path gradients. For small models, a finite-difference check on a shared parameter catches a missing path immediately, because the numerical derivative naturally includes every route from that parameter to the loss.
- A shared trunk feeds three heads with three losses. How does the trunk's update differ from a single-head model's?The trunk's parameters receive the sum of three vector-Jacobian products, so at the same learning rate the effective step on the trunk is larger and grows with the number and weight of the heads. That is why head loss weights and the trunk's learning rate have to be tuned together, and why an unweighted extra head can destabilise a previously stable trunk.
- In a weight-tied language model, what exactly does the tied matrix's gradient consist of?Two summed contributions: a sparse one from the embedding lookup, touching only the rows for tokens present in the batch, and a dense one from the output projection, touching every row because every row participates in the logits. Keeping only one path still trains — silently, towards a different optimum — which is why tying is implemented as one parameter, not two synced copies.
- Why sum the contributions rather than average them?Because the chain rule's total derivative is a sum; averaging would divide the true gradient by the number of consumers, an arbitrary rescale that depends on graph topology rather than on the objective. If a path should count for less, weight its term in the loss explicitly — then summation still gives the exact gradient of the weighted objective you wrote.
A shared parameter is like a road used by three delivery routes. The traffic it carries is the total of all three, not the average and not whichever route reported last.
saying these in an interview costs you the question
- Says the gradients from several uses are averaged
- Thinks the last consumer's gradient overwrites the earlier ones
- Believes a tied weight only gets gradient from one of its two roles
- Updates a shared trunk once per head instead of once per step
- Propagates a node's gradient upstream before all consumers have contributed