Calling backward on a non-scalar output
backward() only works on a single number unless you hand it a weighting vector — and understanding that vector is understanding what autograd really computes.
- 7 min read
- 3 reading levels
- Published
Read these first
On this page 5
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
backward() assigns blame for one single number — give it many numbers, and it demands to know how much each one matters.
Imagine asking a mechanic "why is my car bad?" That question cannot be worked on. Bad at what? Mileage, pickup, seat comfort? The mechanic needs either one complaint, or a weighting: "mostly mileage, a little pickup, ignore the seats." Then the many complaints collapse into one, and work can begin.
backward() is that mechanic. One number — fine. A list of numbers — it refuses, until you say how much each counts.
Why it exists
Blame only makes sense relative to a single goal. "How much did this weight hurt the loss" is answerable, because the loss is one number. "How much did this weight hurt these fifty predictions" is fifty different questions wearing one coat.
That is why every training loop funnels everything into a single loss value before calling backward. When you skip the funnel, PyTorch will not guess your weighting — it stops and asks.
How it works
loss (one number) .backward() works
y (three numbers) .backward() error: which matters?
y (three numbers) .backward(weights) works: y is collapsed
using your weights firstHanding over weights of all ones means "count every output equally" — the same as summing them into one number and blaming that.
A real example you have seen
A restaurant improving its rating cannot act on "make all reviews better". It acts on one number — the average star rating — and every kitchen decision is judged by how it moves that. Collapsing many judgements into one target is what makes improvement possible at all. Loss functions do this for models, which is why they exist.
Remember this
backward()needs one number to assign blame for.- On many numbers, it demands a weighting for collapsing them first.
- Weights of all ones = "treat every output equally" = the same as
.sum().
What to learn next
- Jacobians, vjp and jvp with torch.func — when you genuinely need the whole table.
- In-place operations and when they break autograd — the other classic backward-time error.
- Loss functions — choosing the scalar that deserves the blame.
Developer — Code and libraries.
Setup
pip install torchOutputs verified with torch 2.5.1, CPU.
The refusal, and the two fixes
import torch
x = torch.tensor([1., 2., 3.], requires_grad=True)
y = x * x # y holds 3 numbers, not 1
try:
y.backward()
except RuntimeError as err:
print("refused:", err)
y.backward(torch.ones_like(y)) # weight every output equally
print(x.grad) # 2x, same as y.sum().backward()
x.grad = None # clear before the next experiment
y = x * x
y.backward(torch.tensor([1., 0., 0.])) # care ONLY about y[0]
print(x.grad)refused: grad can be implicitly created only for scalar outputs tensor([2., 4., 6.]) tensor([2., 0., 0.])
The walkthrough
The error names the rule. "Implicitly created" refers to the starting blame: for a scalar, PyTorch seeds the walk with 1.0 automatically. For a vector it will not invent the seed — the argument you pass is that seed.
All-ones gave [2., 4., 6.]. Each y_i is x_i squared and touches only its own x_i, so each input's blame is its own derivative, 2x. Equal weighting reproduces exactly what y.sum().backward() computes — in practice, write the .sum(): clearer intent, same numbers, same speed.
[1., 0., 0.] gave [2., 0., 0.]. Zero weight, zero blame. The seed vector lets you ask about one output of many — this is the mechanism, exposed. What backward computes in general is the weighted combination "seedᵀ times the table of all output-input sensitivities" — the researcher block makes that precise.
The x.grad = None line matters. Gradients accumulate, and without clearing, experiment two would print the sum of both experiments' answers. When your interactive results look mysteriously doubled, this is why.
The place you meet this in real life
Per-sample losses. F.mse_loss(pred, target, reduction="none") returns one loss per sample — a vector. Calling backward on it raises this exact error, and the fix is choosing your weighting consciously: .mean() for the usual average, or a weighted sum when samples deserve different importance:
import torch
pred = torch.tensor([2., 5., 9.], requires_grad=True)
target = torch.tensor([1., 5., 7.])
per_sample = (pred - target) ** 2
weights = torch.tensor([1., 1., 4.]) # third sample counts four times
(per_sample * weights).sum().backward()
print(pred.grad)tensor([ 2., 0., 16.])
The third sample's gradient is 2·(9−7)·4 = 16 — importance weighting, dressed as the backward seed.
Common mistakes
Reflexively writing .sum() to silence the error. It works, but decide between sum and mean: mean divides gradients by the batch size, so learning rates tuned for one do not transfer to the other. Changing reduction mid-project changes your effective learning rate. See loss functions.
Believing y.backward(v) gives "the gradient of y". A vector-valued y has no single gradient — it has a table (one row per output). What you get is v's weighted combination of rows. For the full table, the tools are different: jacrev and friends.
Shape mismatch in the seed. The seed must match y's shape exactly; a wrong-shaped seed raises a mismatch error naming "grad_output". Build it with torch.ones_like(y) and reshape from there.
Forgetting the seed exists when reading others' code. loss.backward() is secretly loss.backward(torch.tensor(1.0)). Knowing that makes gradient scaling tricks — like multiplying the loss by a constant — stop looking magical: the constant rides along on every gradient.
Try it yourself
With y = x * x and x = [1., 2., 3.], first predict x.grad for the seed [0., 1., 0.], then for [2., 2., 2.]. Check both, then confirm the second equals what (2 * y.sum()).backward() produces — and explain why in one sentence.
What to learn next
- Jacobians, vjp and jvp with torch.func — when you genuinely need the whole table.
- In-place operations and when they break autograd — the other classic backward-time error.
- Loss functions — choosing the scalar that deserves the blame.
Researcher — Mathematics and papers.
backward computes vᵀJ, never J
For f: ℝⁿ → ℝᵐ, the Jacobian is J ∈ ℝ^{m×n}, J_{ij} = ∂y_i/∂x_j. y.backward(v) with seed v ∈ ℝᵐ computes:
x.grad += vᵀJ (equivalently Jᵀv ∈ ℝⁿ)
Where v is the grad_tensors/gradient argument, and the scalar case is m = 1, v = 1, reducing vᵀJ to the ordinary gradient ∇f. Reverse-mode AD is structurally a vector–Jacobian product machine: each recorded operation implements its local vᵀJ, and composition follows the chain rule right-to-left. The full Jacobian is never materialised — for a Linear(1000, 1000) layer under batch 32, J would have 32,000 × 32,000 entries; vᵀJ costs one backward pass.
The chain-rule composition (vᵀJ_k)J_{k−1}… proceeds vector-at-a-time, which is exactly why one scalar loss over millions of parameters costs one pass — the cheap gradient principle (Griewank and Walther, 2008, Evaluating Derivatives).
Costs and the m-row question
One backward pass yields one row-combination of J. Recovering all of J by seeding e_1 … e_m costs m backward passes — O(m·cost(f)). Forward mode computes Jv (columns) instead, costing O(n·cost(f)) for the full table. Tall Jacobians (m ≪ n, the deep-learning case) favour reverse; wide ones favour forward. The engineered forms — jacrev, jacfwd, batched via vmap — are treated in Jacobians, vjp and jvp.
The seed as a modelling degree of freedom
Several practical techniques are precisely "choose v ≠ 1":
- Importance/class weighting — v carries per-sample weights, as in the developer demo; identical to weighting the loss.
- Loss scaling in fp16 training — v = s amplifies all adjoints to avoid underflow, un-scaled before the update (Micikevicius et al., 2017, Mixed Precision Training).
- Multi-task balancing — seeding each task head with a tuned weight is equivalent to weighted loss summation; methods like GradNorm (Chen et al., 2018) adapt those weights from gradient magnitudes.
torch.autograd.grad(outputs, inputs, grad_outputs=v)— the functional form, returning vᵀJ without touching.grad, composable for higher-order derivatives (create_graph=True), e.g. gradient-penalty terms in WGAN-GP (Gulrajani et al., 2017).
References
- Griewank and Walther (2008), Evaluating Derivatives, SIAM — VJP/JVP duality, complexity results.
- Baydin et al. (2018), Automatic Differentiation in Machine Learning: a Survey, JMLR — modes and their costs.
- Micikevicius et al. (2017), arXiv:1710.03740 — the seed as loss scale.
What to learn next
- Jacobians, vjp and jvp with torch.func — when you genuinely need the whole table.
- In-place operations and when they break autograd — the other classic backward-time error.
- Loss functions — choosing the scalar that deserves the blame.