← All tutorials

PCGrad (Projecting Conflicting Gradients)

PCGrad removes the conflicting part of each task’s gradient before the gradients are summed, and leaves the rest unchanged.

PCGrad (projecting conflicting gradients), introduced by Yu, Kumar, Gupta, Levine, Hausman and Finn (2020), changes one line of a multitask training step: the sum of the task gradients. Two task gradients conflict when they point more than 90 degrees apart. For every conflicting pair, PCGrad removes from one gradient the part that points against the other, then sums the edited gradients. It runs every training step on that step’s batch and carries no state between steps. Its cost is one backward pass per task over the shared parameters, plus a dot product for each pair of tasks. It learns nothing and adds no hyperparameters. The weighting methods, GradNorm and Uncertainty Weighting, change each loss’s weight; PCGrad changes the gradients’ directions.

On this page

1 Removing the conflicting parts 2 The PCGrad algorithm 3 Conflict test and projection 4 Why this projection 5 Conflicting and non-conflicting pairs 6 Both projections, step by step 7 The tragic triad 8 Implementation in PyTorch 9 Comparison with other methods 10 References

1 Removing the conflicting parts

When two task gradients conflict, adding them cancels part of each, and the sum can raise one task’s loss. PCGrad does not rescale the gradients. It removes only the part of each gradient that points against the other task’s gradient, and keeps every part that does not, so an edited gradient gets shorter only by the part removed, and the sum can come out longer than the plain sum. The paper also calls this gradient surgery.

Plain sumshared weightsg1g2sumAfter PCGradshared weightsg1g2sum
Figure 1. PCGrad (right) removes each conflicting part, so its sum is longer.

2 The PCGrad algorithm

  1. Compute each task’s gradient gi over the shared parameters, one backward pass per task.
  2. For each task i, visit every other task j in random order. If gi conflicts with task j’s original gradient gj, subtract from gi its projection onto gj (Section 3).
  3. Sum the edited gradients and give the result to the optimizer as the shared parameters’ gradient.

3 Conflict test and projection

The test for a conflict is the dot product of two gradients, which equals their lengths times the cosine of the angle between them. Its sign is the sign of that cosine, called the cosine similarity: positive below 90 degrees, zero at 90 degrees, negative above. A negative dot product means a conflict. The part of gi that points along gj is its projection onto gj; in a conflict that projection points against gj, and PCGrad subtracts it. What remains is orthogonal to gj (at 90 degrees). Figure 11 of Introduction to Multitask Learning lets you drag two gradients and watch the projection.

Subtract from gi, task i’s gradient, its projection onto gj, task j’s gradient, but only when the two conflict.
giPC = gi − ⟨gi, gj⟩‖gj‖² gj
giPC is gi minus its projection onto gj. The angle brackets are the dot product, the cosine of the angle times both lengths, so ⟨gi, gj⟩ is negative exactly when the two point more than 90 degrees apart. Dividing by ‖gj‖², the squared length of gj, gives the projection’s length in units of gj, so a longer gj does not make the removed part larger. Multiplying by gj turns it back into a vector. The result is orthogonal to gj, so a small training step with it leaves task j’s loss unchanged to first order.
when ⟨gi, gj⟩ < 0
The projection is removed only when the dot product is negative. At zero or above the gradients do not conflict, and gi is left unchanged.

The core of PCGrad in PyTorch is two functions. project is the eq card in Section 3. pcgrad applies it to every pair in random order and sums the results; G holds one flattened task gradient per row.

import random
import torch

def project(g, g_j):
    dot = g @ g_j
    if dot < 0:  # conflict
        coef = dot / (g_j @ g_j)
        g = g - coef * g_j
    return g

def pcgrad(G):  # G: (K, P)
    K = len(G)
    out = []
    for i in range(K):
        g = G[i].clone()
        others = list(range(K))
        others.remove(i)
        random.shuffle(others)
        for j in others:
            g = project(g, G[j])
        out.append(g)
    return torch.stack(out).sum(0)
  1. 1The conflict test and the projection, lines 2 and 1 of the eq card in Section 3.
  2. 2Every other task in a new random order each call, step 2 in Section 2. The authors’ TensorFlow code calls a shuffle whose result is discarded, so its order is fixed in practice; LibMTL shuffles. With two tasks the order does not matter.
  3. 3Tests and projects against each task’s original G[j], as the paper’s pseudocode does, then sums, step 3 in Section 2. A mean instead of the sum would divide the step by K.

4 Why this projection

For a small step, only the projection changes task j’s loss, so it is the smallest edit that removes the conflict.

gjgigiPC kept,90° to task jpart along task jremovedremove less → conflict staysremove more → task i loses help
Figure 2. Only the red part changes task j’s loss; PCGrad removes exactly that.
  1. Why edit both gradients? Editing only one leaves the other still conflicting with it. With two tasks, editing both leaves nothing in the sum that conflicts with either task. With three or more, a later projection can bring part of a conflict back.
  2. Why a random order? With three or more tasks the result depends on the order of the projections. A new random order each step spreads that effect over the tasks.

5 Conflicting and non-conflicting pairs

PCGrad paper figure: conflicting gradients and the projection surgery in four panels

The paper’s Figure 2. arXiv:2001.06782

Panel (a), two conflicting task gradients
Panel (b), task i's gradient projected onto the normal of task j's gradient

Panels (a) and (b).

Panel (c), task j's gradient projected onto the normal of task i's gradient
Panel (d), a non-conflicting pair left unchanged

Panels (c) and (d). From the paper’s Figure 2, arXiv:2001.06782.

The original caption reads “… PCGrad projects task i's gradient onto the normal vector of task j's gradient, and vice versa. …”

Figure 3. (a) conflict; (b) gi minus its part along gj; (c) the reverse; (d) no conflict.

6 Both projections, step by step

⟨g1, g2⟩ < 0
g1
g2
g1 projected
g2 projected
conflicting
part
conflicting
part
plain sum
d, the combined
gradient
Check for a conflict (step 1 of 4)
giPC = gi − ⟨gi, gj⟩‖gj‖²gj
The eq card in Section 3: gi loses its projection onto gj, drawn in red.
when ⟨gi, gj⟩ < 0
Only a negative dot product triggers the removal, as in Section 3.
d = g1PC + g2PC
The combined gradient d is the sum of the two projected gradients, and the training step moves against it. Each projected gradient is orthogonal to the other task’s original gradient, and only the conflicting parts were removed from the plain sum.
⟨gi, gj⟩‖gj‖² gj the conflicting partgi task i's gradientgj task j's gradient⟨gi, gj⟩ < 0 the conflict testgiPC the projected gradientd the combined gradient, the gold arrow⟨ , ⟩ the dot product‖gj‖² the squared length of gji, j the two tasks, both ways

Figure 4. Red marks each conflicting part, gold the combined gradient d.

7 The tragic triad

Joint training can do worse than training each task alone. The paper attributes this to conflicting gradients, and argues the damage is large when three conditions hold together, which it calls the tragic triad.

> 90°Conflicting gradientsnegative cosine similarity,more than 90° apartDominating gradientsone gradient much longerthan the otherstarthigherHigh curvatureslope changes within one step,so a step can raise a loss
Figure 5. The tragic triad; the damage is large when all three hold together.
What the paper proves about one PCGrad step and about where PCGrad ends up.
L(θ−ηgPC) ≤ L(θ−ηg)
From the same shared parameters θ, a step of size η along the PCGrad sum gPC ends at a total loss L no higher than the same step along the plain sum g of the task gradients. The paper proves it under three conditions, the triad of Figure 5 made precise: the gradients conflict strongly enough (a milder conflict suffices when their lengths differ more), the curvature is high enough, and the step η is large enough.
θt → θ*
Theorem 1: for two tasks with convex losses and a small enough η, the parameters θt after t PCGrad steps converge to the optimum θ*.
or cos(g1, g2) = −1
Or they stop at a point where the two gradients point in exactly opposite directions, a cosine of −1. Theorem 4 in Appendix A.4 extends this to heavy-ball momentum for strongly convex, smooth losses with tuned step sizes; Section 8.1 of Gradient Balancing Inside the Training Loop shows what momentum does to the removed part.
Benchmark in the paperResult with PCGrad
Meta-World robot tasks (10 and 50 tasks), one shared policy trained with multitask SAC (soft actor-critic)Average success rate more than 30 percentage points higher than the same training without PCGrad

8 Implementation in PyTorch

PCGrad replaces the usual single loss.backward(). The step below differentiates each task’s batch-mean loss separately over the shared parameters, edits the results with pcgrad and project from Section 3, and writes the edited sum into the shared parameters’ gradients.

import torch
from torch.autograd import grad

RETAIN = dict(retain_graph=True)

def flat_grad(loss, ps):
    gs = grad(loss, ps, **RETAIN)
    flat = [g.ravel() for g in gs]
    return torch.cat(flat)

def fill_grads(params, vec):
    i = 0
    for p in params:
        n = p.numel()
        part = vec[i:i + n]
        p.grad = part.view_as(p)
        i += n

body = list(trunk.parameters())
head_ps = list(heads.parameters())

def pcgrad_step(losses):
    rows = []
    for L in losses:
        g_k = flat_grad(L, body)
        rows.append(g_k)
    g = pcgrad(torch.stack(rows))
    opt.zero_grad()
    fill_grads(body, g)
    total = sum(losses)
    total.backward(inputs=head_ps)
    opt.step()
  1. 1flat_grad returns one task’s gradient over a parameter list ps as one flat vector, keeping the graph for the next task.
  2. 2fill_grads writes a flat vector back into each parameter’s .grad, using numel and view_as.
  3. 3K backward passes over the shared parameters body and K flattened gradient copies, step 1 in Section 2.
  4. 4PCGrad replaces the plain sum, at K(K−1) dot products; the result becomes the shared gradient, step 3 in Section 2. Mixed precision multiplies every loss by one scale, so divide the gradients by it before projecting (Section 11 of Gradient Balancing Inside the Training Loop), and clip after fill_grads.
  5. 5The heads get their ordinary gradients, since each head receives only its own task’s loss (Section 3 of Gradient Balancing Inside the Training Loop shows this write-back style), then one optimizer step for all parameters.

The official code is tianheyu927/PCGrad, a TensorFlow 1 optimizer wrapper used as PCGrad(tf.train.AdamOptimizer()) followed by optimizer.minimize(losses) with a list of task losses. PCGrad adds no hyperparameters; the paper keeps the base method’s settings. LibMTL ships a PyTorch version (--weighting PCGrad) that edits only the shared encoder’s gradients and shuffles the task order.

Loss scale and momentum still matter. A loss 10 times larger gives a gradient 10 times longer, and momentum can bring back a removed direction; see Sections 5 and 8.1 of Gradient Balancing Inside the Training Loop.

1 · Forwardeach task computes its loss2 · Backwardper-task gradients, shared layers3 · Balancecombine the task gradients4 · Clipcap the gradient's length5 · Optimizer stepupdate the network's parameters6 · Logrecord what this step didloss-weighting methods act heregradient methods act here
Figure 6. PCGrad replaces the plain sum; clipping and the optimizer act on its result.

9 Comparison with other methods

Two methods learn each task’s loss weight (GradNorm and Uncertainty Weighting). Four methods edit the task gradients (PCGrad, GradVac, CAGrad and MGDA). The question each one answers:

MethodThe question it answers
GradNormHow much weight should each task's loss get so all tasks train at a similar rate? (learned during training from how fast each loss falls)
Uncertainty WeightingHow much weight should each task's loss get, given how noisy that task's targets are? (the noise level is learned along with the model)
PCGrad (this page)How should a task gradient be edited when it conflicts with another task's gradient?
GradVacHow should each pair of task gradients be moved toward a target cosine, a running average of that pair's past cosines?
CAGradHow can a step raise the smallest per-task improvement while staying near the average gradient?
MGDAWhich convex combination of task gradients lowers every task's loss at once, and when does no such combination exist?

PCGrad leaves each edited gradient orthogonal to the one it conflicted with, a cosine similarity of zero. GradVac asks whether zero is the right target for every pair of tasks.

10 References

  1. Yu, T., et al. (2020). Gradient Surgery for Multi-Task Learning. NeurIPS. arXiv:2001.06782.