Skip to content
AI.info

Training and optimization

Computational Graphs and the Gradient Signal

Trace how forward operations, parameter sharing, branching, stopping, and numerical choices shape the gradients received by each parameter.

By the end you can

Visual

A gradient follows executed dependencies, not intentions

This graph shows why a parameter receives only the signal connected to it. Cut one edge and the rest of the program still runs. It still reports a falling loss, and it still trains nothing that mattered.

SimSiam is the measured case. Chen and He held the architecture and every hyper-parameter fixed and removed one stop-gradient. ImageNet linear-evaluation top-1 accuracy moved from 67.7±0.1% to 0.1%, chance level, while the training loss fell to −1, its minimum possible value. Nothing in the optimizer changed. Only the graph did.

FigureProcess · 5 steps
  1. 1

    Inputs and state

    A batch, parameters, masks, and mode flags enter the forward program.

  2. 2

    Intermediate operations

    Layers, normalizations, branches, and losses create dependent values.

  3. 3

    Scalar objective

    Reductions combine selected outputs into the value being differentiated.

  4. 4

    Reverse accumulation

    Local derivatives are multiplied and added along graph paths.

  5. 5

    Parameter gradients

    Each trainable tensor receives the sum of connected contributions.

Comparison

Zero, missing, and tiny gradients mean different things

A debugger should preserve these distinctions, because the three columns have different causes and different repairs. The rightmost one is misread most often, and two published measurements show what it actually looks like.

Start with precision. A histogram of activation gradients, collected across all layers while the Multibox SSD detector trained in FP32, found 67% of the values already at zero. Much of what remained sat in the band [2^-27, 2^-24), below anything half precision can represent. Micikevicius and his co-authors, at ICLR 2018, state the consequence in one sentence: “Note that much of the FP16 representable range was left unused, while many values were below the minimum representable range and became zeros.” The same network diverged in half precision with no loss scaling. Multiplying the loss by 8 before backpropagation gave 77.1% mAP against a 76.9% FP32 baseline. PyTorch's Automatic Mixed Precision documentation states the mechanism in one clause: such values “flush to zero ("underflow"), so the update for the corresponding parameters will be lost”. A tensor of exact zeros in that regime is not a saturated derivative. It is a number that did not fit.

Depth compounds the same loss without any help from precision. He and his co-authors trained a 30-layer model — 27 conv layers and 3 fc layers. At a fixed initialization std of 0.01 they computed that the gradient std propagated from conv10 back to conv2 is 1/(1.7 × 10^4) of the value their derivation prescribes. What that costs is in their ICCV 2015 paper: “Our initialization is able to make the extremely deep model converge. On the contrary, the “Xavier” method completely stalls the learning, and the gradients are diminishing as monitored in the experiments.” Eight layers of attenuation is the distance between a model that trains and one that never starts. On a norm plot it appears only as the third column of this comparison: small, finite, easy to dismiss.

FigureComparison · 3 columns

No gradient object

The parameter was not connected to the differentiated objective.

  • Cause: unused or detached path
  • Signal: gradient is absent
  • Question: should this parameter participate?
  • Check: graph and requires-grad state

Exact zero gradient

The path exists but local derivatives or cancellation yield zero.

  • Cause: saturation, masks, symmetry
  • Signal: tensor of zeros
  • Question: is zero expected here?
  • Check: activations and local Jacobians

Very small gradient

The signal survives but may be attenuated or underflowed.

  • Cause: depth, scale, precision
  • Signal: tiny finite norms
  • Question: can updates move parameters?
  • Check: layerwise norm ratios

Reverse mode asks how one scalar changes with many parameters

Training usually differentiates one reduced objective with respect to millions of parameters. Reverse-mode automatic differentiation reuses intermediate dependencies efficiently for that direction. The system computes derivatives of the executed program. A conditional branch, detached tensor, in-place mutation, or train/eval mode can therefore change the graph and its gradients.

The efficiency has a number on it. A survey by Baydin and co-authors, in the Journal of Machine Learning Research, states that for f : Rn → Rm “the time it takes to calculate the m×n Jacobian by the forward mode is n c ops(f), whereas the same computation can be done via reverse mode in m c ops(f), where c is a constant guaranteed to be c < 6 and typically c ∼ [2, 3]”. A single scalar loss makes m equal to one. So the gradient of a billion parameters costs a few forward passes, and nothing in that bound depends on n. The cheapness is the reason nobody inspects the graph until it breaks.

Autodiff differentiates what ran, not what the programmer meant to run.

Example

Four graph bugs that look like optimizer problems

These failures often survive shape checks and forward unit tests. Four of the five below have been isolated in published experiments where the graph was the only thing that changed.

  • Detached feature: An auxiliary head consumes a stopped-gradient representation, so its loss never trains the shared encoder. Chen and He ran the controlled version, and their CVPR 2021 report of it is one sentence: “Solely removing stop-gradient, the accuracy becomes 0.1%, which is the chance-level guess in ImageNet.” With the stop-gradient in place the same model reaches 67.7±0.1% top-1. Without it the training loss collapses to −1, its minimum possible value — exactly the reading a loss curve cannot distinguish from success. BYOL shows the same shape independently: in its Table 5, variants lacking the target network or the predictor score 0.3%, 0.2% and 0.1% against 72.5% for BYOL itself. Detachment can be load-bearing as well as accidental. The graph does not say which you intended.
  • Unused branch: A routing condition prevents one expert from receiving examples, leaving its parameters with zero gradients. The forward pass is valid, the loss is finite, and the parameters are simply never addressed — a coverage fact about the batch, not a statement about the optimum.
  • Shared parameter: The same embedding table appears in two branches, so its gradient is the sum of both responsibilities. Press and Wolf tied a language model's input and output embeddings into a single matrix and analysed that summed update rule directly. Their abstract reads: “We analyze the resulting update rules and show that the tied embedding evolves in a more similar way to the output embedding than to the input embedding in the untied model.” On Penn Treebank, with a Recurrent Highway Network and Bayesian dropout, the tied model used 24M parameters instead of 32M and reached a test perplexity of 66.0 against 68.5 untied. Inan and co-authors derived the same tying concurrently and independently at ICLR 2017. The summed gradient is not a hazard to be avoided; it is a design decision to be made deliberately.
  • Non-differentiable decision: An argmax selects a hard path, removing useful local information unless a different estimator is designed. Jang and co-authors designed one at ICLR 2017: a Gumbel-Softmax relaxation of the categorical sample, with a straight-through variant that discretizes with argmax in the forward pass while differentiating the continuous approximation. On semi-supervised binarized MNIST (100 labeled, 50,000 unlabeled examples) ST Gumbel-Softmax reached 93.6% unlabeled test accuracy and plain Gumbel-Softmax 92.4%, against 92.6% for marginalization. It is also faster: “Training the model with the Gumbel-Softmax estimator is 2× as fast for 10 classes and 9.9× as fast for 100 classes.” PyTorch's torch.nn.functional.gumbel_softmax writes the straight-through trick as y_hard - y_soft.detach() + y_soft — the same detach that silently severs a graph, used on purpose to keep one alive.
  • Mode mismatch: Dropout or normalization behaves differently during a diagnostic pass, changing both activations and gradient statistics. This is designed behaviour, not a framework quirk. Ioffe and Szegedy said so in 2015, when they introduced Batch Normalization: “The normalization of activations that depends on the mini-batch allows efficient training, but is neither necessary nor desirable during inference; we want the output to depend only on the input, deterministically.” PyTorch's torch.nn.BatchNorm2d implements that switch with running estimates, kept at a default momentum of 0.1 and a default eps of 1e-5, used for normalization during evaluation. A pass run in the wrong mode is differentiating a different function.

Key idea

A mathematically correct gradient can still teach the wrong behavior

Autodiff may perfectly compute the derivative of a leaked, badly reduced, or poorly chosen objective. A correct gradient is therefore necessary but not sufficient.

The model may also sit in a region where useful directions are tiny, noisy, or dominated by another term. Debug the objective and data alongside the graph. PyTorch ships a finite-difference check for exactly this boundary: torch.autograd.gradcheck compares “gradients computed via small finite differences against analytical gradients”, with defaults of eps=1e-6, atol=1e-5 and rtol=1e-3. A custom layer that passes gradcheck has a correct derivative to within those tolerances. It says nothing about whether the objective is the right one. The SimSiam ablation is the proof: a model differentiating a perfectly correct loss all the way down to −1 while learning nothing at all.

Derivative accuracy cannot validate the meaning of the objective.

Steps

Test gradient flow before a long run

Use small deterministic cases that show clearly which parameter answers for which output. Step 5 is the one that gets skipped, and Batch Normalization is the reason not to skip it. Training normalizes with mini-batch statistics, inference with population statistics. The two modes are two functions, not one function in different clothes. Ioffe and Szegedy reported that the technique matched the prior state of the art with 14 times fewer training steps, and that an ensemble reached 4.82% top-5 test error. That much performance riding on a mode flag means a diagnostic pass taken in the wrong mode is not a noisy measurement of your model. It is a clean measurement of a different one.

FigureProcess · 5 steps
  1. 1. Overfit a tiny batch

    Confirm that the complete graph can reduce loss on a handful of examples.

  2. 2. Inspect parameter coverage

    List trainable tensors with missing, zero, finite, or invalid gradients.

  3. 3. Compare finite differences

    Check selected scalar parameters or small tensors away from non-smooth points.

  4. 4. Isolate branches

    Activate one loss or route at a time and inspect expected gradient destinations.

  5. 5. Test mode changes

    Compare training and evaluation behavior where normalization, dropout, or caching matters.

Tracing responsibility through a supply chain

A defective component can be traced backward through suppliers and assembly stages. A shared supplier can contribute to several products, while an unused supplier receives no responsibility.

An audit like that ends in fault. Backpropagation ends in a number for how much a small change in one parameter would have moved the loss. That is neither blame nor proof of cause. What it does explain is branching, accumulation, and disconnected paths — the tied embedding that answers for two uses at once, and the detached encoder that answers for none.

Backpropagation assigns local sensitivity along recorded dependencies.

Layerwise ratios reveal more than a global norm

A single global gradient norm can hide dead layers and one dominating tensor. Track distributions by module, parameter type, and task contribution.

Two per-layer statistics can disagree about the same network. Glorot and Bengio monitored activations and gradients layer by layer after standard initialization. The variance of the back-propagated gradients shrank as it moved toward the input. The variance of the weight gradients stayed roughly constant across layers. They wrote: “What was initially really surprising is that even when the back-propagated gradients become smaller (standard initialization), the variance of the weights gradients is roughly constant across layers, as shown on Figure 8.” The caption of that figure puts it flatly: “Even though with standard initialization the back-propagated gradients get smaller, the weight gradients do not!” Two honest diagnostics, one network, opposite verdicts. A single averaged number would have reported neither.

The graph records operations, not intent

Useful diagnostics include gradient-to-parameter norm ratios, update-to-parameter ratios, activation statistics, and the fraction of missing or non-finite values. Read them per module. The ratio that identifies a stalled model in that 30-layer network is the one taken between two named conv layers, not the one taken over the whole net.

One default catches nearly everyone once. PyTorch's backward accumulates gradients in the leaves rather than replacing them. Its documentation for torch.Tensor.backward warns that you “might need to zero .grad attributes or set them to None before calling it”. A forgotten zero_grad turns the training loop into a running sum. The curve then looks like a learning-rate problem, and it is not one. That is the pattern this whole lesson repeats: an execution fact about the graph, wearing the costume of an optimizer failure.

Gradient observability should follow the structure of the model, not flatten it into one number.

Key takeaways