← All tutorials

CAGrad (Conflict-Averse Gradient Descent)

Improve the task each step helps least, while still lowering the average loss.

On this page

1 What CAGrad does each step 2 A ball around the average gradient 3 The worst-case improvement objective 4 Solving for one weight per task 5 The CAGrad update 6 One CAGrad update, step by step 7 Convergence guarantee 8 Toy trajectories in loss space 9 Six methods on the toy problem 10 Results over five starts 11 When to use CAGrad, and its cost 12 CAGrad in a PyTorch training step 13 Comparison with other multitask methods 14 References
ProblemThe average step can help one task and hurt another.
MethodEach step, weight the tasks so the least-helped one gains most, and move the average gradient along that blend’s direction by a set fraction of its length.
GuaranteeWhile that fraction stays below 1, the average loss’s gradient provably shrinks toward zero.
CostOne backward pass per task each step, plus a small solve over the task weights.

1 What CAGrad does each step

A task’s loss measures how badly the model performs that task; its gradient points where that loss rises fastest, and training steps against it.

Each training step, CAGrad takes the gradient gi of each task i over the shared weights, one entry per shared parameter, often millions (the figures draw two entries). It keeps training on the average loss and changes only the update’s direction, in three moves.

  1. It searches a ball around the average gradient, the vectors within one radius of it. The radius is a set fraction of that gradient’s length, so the ball grows and shrinks with it (Section 2).
  2. In the ball it looks for the update that most helps the least-helped task, the one whose loss drops least (Section 3).
  3. That search comes down to choosing one weight per task, each zero or more and adding up to 1, so the least-helped task gains most. The weighted task gradients add up to the weighted gradient, and the update moves from the average gradient one radius along its direction (Section 4, Section 5).

CAGrad keeps no state between steps; the official CAGrad code restarts the weight solve from equal weights each step.

2 A ball around the average gradient

The average gradient g0, the mean of the task gradients, can raise one task’s loss.

Averaging can raise one task’s lossg1, task 1g2, task 2g0, average90° from g2
Figure 1. g0 is over 90° from g2, so a step along it raises task 2’s loss.

Searching only the ball around g0 keeps the update near g0, so the average loss still falls. ‖g0‖ is the length of g0. The hyperparameter c sets the radius as a fraction of that length, so the radius is c‖g0‖.

At c = 0 CAGrad is plain gradient descent (GD); very large c gives the direction MGDA (Multiple Gradient Descent Algorithm) picks, the shortest weighted average of the task gradients. Raising c favors the least-helped task; lowering it stays near the average.

The ball as c growsaveragegradientballc = 0plain gradientdescentin betweenCAGrad'suseful rangec very largeMGDA,ball unbounded123
Figure 2. 1 c = 0 is plain GD, 2 CAGrad is in between, 3 very large c is MGDA.
Values of c used in practiceNYUv2indoor scenes, 3 tasks0.4Cityscapesstreet scenes, 2 tasks0.20.4, 0.6Meta-World, 10 tasksrobot-arm control0.9Meta-World, 50 tasksrobot-arm control0.5Multi-Fashion+MNISTclothing over a digit0.2LibMTL defaultPyTorch library0.500.51c < 1 for the guarantee
Figure 3. All sit below the dashed limit at 1. For Cityscapes the paper’s text gives 0.2 (hollow dot); its run script tries 0.4 and 0.6.

3 The worst-case improvement objective

A candidate direction d works like a gradient: parameters move against it.

Both pairs are illustrative, with c = 0.5. The NYUv2 pair is one network on indoor photos learning seg (semantic segmentation, a class per pixel) and depth (meters per pixel); the other is the pair Figure 4 draws. A task’s improvement is how much a small step against d lowers its loss.

NYUv2Figure 4
g1, g2(2, 1),
(−6, 2)
(3, 1),
(−1, 2)
average g0(−2, 1.5)(1, 1.5)
radius c‖g0‖1.250.901
improvements along g0−2.5 and 154.50 and 2.00
worst-case improvement−2.5, seg’s loss rises2.00
CAGrad looks for the step that helps its least-helped task the most, without straying far from the average gradient.
maxd mini⟨gi, d⟩
For a small step against d, ⟨gi, d⟩ is approximately how much task i’s loss drops per unit of step size, its improvement. The angle brackets are the dot product: multiply the two vectors entry by entry and add. It is large when d points along gi, zero at right angles, and negative when the step raises task i’s loss. min over i keeps the smallest per-task improvement, the worst-case improvement. max over d picks the direction where it is largest.
s.t. ‖d − g0‖ ≤ c‖g0‖
s.t. means such that. The constraint keeps d within c‖g0‖ of g0, so d stays inside the ball. While c is below 1, every direction in the ball still lowers the average loss, to first order. If some direction in the ball lowers every task’s loss, CAGrad’s choice does too; with small c none may exist, and the least-helped task’s loss can rise.
So CAGrad trades a little average progress for the worst task’s sake: on NYUv2 the average step gives seg −2.5, so CAGrad searches the ball of radius 1.25 for a direction whose worst-case improvement is higher.

Best direction in the ball

Figure 4. Drag d to the ring, where the least-helped task gains most.

Worst-case improvement 2.00
Task 1
4.50
Task 2
2.00 least helped

4 Solving for one weight per task

A direction d has millions of entries, so CAGrad solves its dual problem instead: the same answer, found by searching over one weight per task. The weighted sum of the task gradients is the weighted gradient gw.

Every weighted gradient lies on one lineg1g2optimalgw*g0, equal weights123
Figure 5. 1 Equal weights, 2 CAGrad’s weights, all on task 2, 3 any blend.

The weights solved for the two illustrative pairs of Section 3.

NYUv2Figure 4
solved weightsall on segall on task 2
bound at the solved weights0.2954.02
Instead of searching every direction, CAGrad solves for one weight per task and scores each weight list by the best value it allows.
gw = w1g1 + w2g2
This replaces the search over directions of Section 3. w1 and w2 are the task weights. Each is zero or more, and they add up to 1. So gw is a weighted average of the task gradients g1 and g2. The line shows two tasks, as in Figure 5. K is the number of tasks; in general the sum runs over all K.
w* = argminw∈Δ F(w)
F(w) is the largest value of ⟨gw, d⟩ over directions d in the ball. ⟨gw, d⟩ is a weighted average of the task improvements, so it is never below the worst one. So F(w) is an upper bound on the best worst-case improvement. Δ is the set of allowed weights, and w ∈ Δ (read ∈ as “is in”) says w is one of them. argmin picks the weights w* with the lowest upper bound. The paper shows this bound is reached, so w* also solves the search over directions.
F(w) = ⟨gw, g0⟩
At the ball’s center, the direction g0 gives gw the improvement ⟨gw, g0⟩. This term is small when the weights favor the task g0 helps least. So on its own it moves the weights toward that task.
+ c‖g0‖‖gw‖
Moving the full radius c‖g0‖ along gw adds the radius times the length of gw. This term favors short weighted gradients. As c grows it dominates, and w* approaches the weights of the shortest weighted gradient, MGDA’s choice.
So the weights say which task the step must lean toward: on NYUv2 all the weight goes to seg, the task the average step hurt, and F(w*) = 0.295.

G holds one task gradient per row. solve_weights takes the Gram matrix GG (the K×K table of dot products between task gradients) and c and returns w*.

import numpy as np
from scipy import optimize

def F(w, GG, c):
    g0_len = np.sqrt(GG.mean())
    center = w @ GG.mean(axis=1)
    gw_sq = max(w @ GG @ w, 0.0)
    gw_len = np.sqrt(gw_sq)
    ball = c * g0_len * gw_len
    return center + ball

CONSTRAINT = {"type": "eq", "fun": lambda w: w.sum() - 1}

def solve_weights(GG, c):
    K = len(GG)
    res = optimize.minimize(F, np.full(K, 1 / K), args=(GG, c), method="SLSQP",
                            bounds=[(0, 1)] * K, constraints=CONSTRAINT)
    return res.x

g1 = [3.0, 1.0]
g2 = [-1.0, 2.0]
G = np.array([g1, g2])  # (K, P)
c = 0.5
w_star = solve_weights(G @ G.T, c)
  1. 1Computes F(w) from GG, lines 3 and 4 of the weight solve in Section 4; max stops rounding from going below zero.
  2. 2Keeps the weights adding up to 1, the set Δ in line 2 of that solve.
  3. 3Minimizes F from equal weights, each in [0, 1], the argmin in line 2.
  4. 4Uses the two tasks of Figure 5 and returns w* = (0, 1).

5 The CAGrad update

In the illustrative NYUv2 pair, w* puts all the weight on seg, so the blend is gseg = (2, 1), of direction (0.894, 0.447).

value
start, the average g0(−2, 1.5)
one radius along seg1.25 × (0.894, 0.447)
step d*(−0.882, 2.059)
improvements, seg and depth0.295 and 9.410
The step starts at the average gradient and moves one full radius along the solved blend’s direction.
d* = g0
w* is the weight solve of Section 4. d* is the update direction, the star marking a solved quantity. It starts at the ball’s center, g0.
+ c‖g0‖ gw*‖gw*‖
The fraction is gw*, the weighted gradient built from w*, divided by its own length, so only its direction counts. Multiplied by the radius c‖g0‖, the step is exactly one radius long, so d* lands on the ball’s edge. This d* is the best point in Figure 4. The exception is a gw* of length zero, where the weighted task gradients cancel. That point is Pareto stationary: no small step lowers every task’s loss at once.
So the least-helped task goes from −2.5 to 0.295, and both improvements are now positive.

Continuing from the solver of Section 4, this code turns w* into d*.

g_w = w_star @ G  # optimal g_w
g0 = G.mean(axis=0)
radius = c * np.linalg.norm(g0)
EPS = 1e-12  # zero-length guard
gw_len = np.linalg.norm(g_w)
unit = g_w / (gw_len + EPS)
d_star = g0 + radius * unit
  1. 1Builds gw* from w*, line 1 of the weight solve in Section 4.
  2. 2The center g0, line 1 of the update in Section 5, and the radius c‖g0‖, line 2.
  3. 3The fraction in line 2, guarded against length zero.
  4. 4One radius out to d*, the best point in Figure 4.

6 One CAGrad update, step by step

The walkthrough in Figure 6 uses the illustrative pair Figure 4 draws, g1 = (3, 1) and g2 = (−1, 2), with c = 0.5.

at the centerat d*
weightsequalall on task 2
improvements4.50 and 2.004.10 and 4.02
worst-case improvement2.004.02

Figure 6. The four steps of one CAGrad update: find the least-helped task, draw the ball, solve the weights, move to the ball’s edge.

One CAGrad update finds the best worst-case direction inside the ball by solving one weight per task.
maxd mini⟨gi, d⟩
This is the objective of Section 3. Each direction d gets the smaller of its two task improvements; the largest such value wins.
s.t. ‖d − g0‖ ≤ c‖g0‖
The shaded region is the ball of radius c‖g0‖ around g0, with c = 0.5 here.
gw = w1g1 + w2g2
This starts the weight solve of Section 4. The bars show the weights w1 and w2, which add up to 1.
w* = argminw∈Δ F(w)
Solving moves all the weight onto task 2, and the worst-case improvement readout rises to its highest value.
F(w) = ⟨gw, g0⟩
This term is the improvement gw gets at the ball’s center.
+ c‖g0‖‖gw‖
This term adds the radius times the length of gw.
d* = g0
This is the update of Section 5. The arrow starts at the center g0.
+ c‖g0‖ gw*‖gw*‖
It moves one radius along the direction of g2 and ends on the ball’s edge at d*.
So the walkthrough runs the four steps as one update, and the worst-case improvement rises from 2.00 to 4.02.
d* the update directiong0 the average gradientc the radius as a fraction of ‖g0‖w1, g1 task 1’s weight and gradientw2, g2 task 2’s weight and gradient⟨gi, d⟩ task i’s improvement along d

7 Convergence guarantee

CAGrad provably reaches a stationary point of the average loss (zero gradient, possibly a saddle point), assuming H-Lipschitz gradients (the gradient changes at most H times as fast as the parameters), a loss bounded below, and step size α ≤ 1/H.

Take c = 0.5, the value in the worked example. The convergence bound has 1 − c² in its denominator, and 1 − 0.5² = 0.75 where plain gradient descent (c = 0) has 1. So CAGrad’s bound is 1 ÷ 0.75 = 1.333 times plain gradient descent’s.

Summed over all steps, the average gradient’s squared length stays below a fixed bound, so CAGrad still converges on the average loss.
Σt=0T ‖∇L0(θt)‖²
This sums the squared length of the average gradient while the update of Section 5 runs every step; c is the radius factor of Section 2. L0 is the average loss and θt the parameters at step t, so ∇L0(θt) is the average gradient at step t; the double bars squared give its squared length. Σ adds these over steps t = 0 to T.
≤ 2(L0(θ0) − L0*)α(1 − c²)
L0* is the lowest the average loss can go. Only c < 1 keeps the denominator positive. The bound stays fixed as T grows, so the average gradient’s length must shrink toward zero. A larger c raises the bound; that is the cost of favoring the least-helped task.
So caring for the least-helped task costs only a constant factor in convergence: in the c = 0.5 example the denominator holds 0.75 instead of 1, and the bound is 1.333 times plain gradient descent’s.
  1. Other proofs are weaker. MGDA proves only a Pareto-stationary end point; PCGrad, which removes the part of each task gradient that conflicts with another task’s, has a proof that covers two tasks with convex (bowl-shaped) losses.
  2. At c of 1 or more, the guarantee is gone. The update can then settle where the average loss’s gradient is not zero, at a point that is only Pareto stationary.

8 Toy trajectories in loss space

The paper’s toy has two parameters and two task losses, L1 and L2, run from five starts. Figures 7 to 9 plot loss space, one axis per task loss, with each start’s trail. The gray Pareto front holds the loss pairs where neither loss can drop without the other rising; the star is its lowest average loss.

Key for the paper's Figure 3, the Pareto set, initial point, and the star marking the best point
Panel 1, GD trajectories in loss space
Panel 2, MGDA trajectories in loss space
L1 across, L2 up

Figure 7. Trails fade orange to yellow. GD stalls on 2 of 5 starts; MGDA stops where it first meets the front. Panels 1 and 2 of the paper’s Figure 3.

Panel 3, PCGrad trajectories in loss space
Panel 4, CAGrad at c 0.5 trajectories in loss space
L1 across, L2 up

Figure 8. PCGrad also stops at the front; CAGrad, c = 0.5, takes all five to the star. Panels 3 and 4 of the paper’s Figure 3.

Panel 5, CAGrad with c at 0 and 10 from one hard start
Panel 6, CAGrad with c at 0.2, 0.5 and 0.8 from one hard start
L1 across, L2 up

Figure 9. From one hard start, c = 0 stalls; c = 10 and 0.2 to 0.8 reach the star. Panels 5 and 6 of the paper’s Figure 3.

9 Six methods on the toy problem

The race in Figure 10 runs six methods on the paper’s toy code: each loss has a narrow curved valley in the upper half and a bowl in the lower half, with a ridge between. The Pareto set holds the parameters whose losses lie on the front.

All six use Adam (each parameter’s step scaled by its recent gradient size) for 60,000 steps. GradVac moves task gradients that agree less than usual toward a target angle. GradNorm reweights the task losses so they train at similar rates. The race runs the official toy code. That code divides CAGrad’s step by 1 + c, and one sign in its second loss differs from the formula printed in the paper.

Start

Done at step 60,000.

Figure 10. 1 The star, 2 GD’s end, 3 the Pareto set.

10 Results over five starts

Where each method ends, out of 5 startson the Pareto setat the starGD (plain average)3MGDA1PCGrad1GradNorm1GradVac5CAGrad, c = 0.5505
Figure 11. Over all five starts, only GradVac and CAGrad always reach the star.

Of the two, only CAGrad has a convergence proof (Section 7). This GradNorm run is approximate, since GradNorm divides each loss by its starting value and these toy losses go below zero.

11 When to use CAGrad, and its cost

  1. Use it when task gradients conflict and the average loss still matters.
  2. Time per step is K backward passes plus the small solve. CAGrad-Fast keeps the guarantee while sampling a random task subset per step, 4 of 10 and 8 of 50 on Meta-World (Figure 12).
  3. Memory holds K flattened shared gradients plus each task’s computation graph (what autograd keeps for a backward pass).
CAGrad-Fast speed over PCGradMeta-World, 10 tasksabout 2×Meta-World, 50 tasksabout 5×1×, same as PCGrad
Figure 12. CAGrad-Fast’s speedup over PCGrad grows with the number of tasks.

12 CAGrad in a PyTorch training step

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 13. CAGrad acts after the backward pass, before clipping and the optimizer step.

The listing stacks the task gradients into G (Section 3 of Gradient Balancing Inside the Training Loop shows how each is computed), calls solve_weights (Section 4), and writes d* into the .grad of body, the shared backbone’s parameters. heads, each task’s output layers, get the average loss’s gradient.

import torch
from torch.nn.utils import clip_grad_norm_

c = 0.4  # radius c (NYUv2)
EPS = 1e-12  # zero-length guard

def flat_grad(loss, body):
    grads = torch.autograd.grad(loss, body, retain_graph=True)
    return torch.cat([g.ravel() for g in grads])

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

def cagrad(losses, parts, opt):
    body, heads = parts
    K = len(losses)
    rows = [flat_grad(L, body) for L in losses]
    G = torch.stack(rows)  # K x P
    GG = (G @ G.T).cpu().numpy()
    w_star = solve_weights(GG, c)
    w = torch.tensor(w_star).to(G)
    g0 = G.mean(dim=0)
    g_w = w @ G
    gw_len = g_w.norm()
    unit = g_w / (gw_len + EPS)
    radius = c * g0.norm()
    d_star = g0 + radius * unit
    opt.zero_grad()
    fill_grads(body, d_star)
    avg = sum(losses) / K
    avg.backward(inputs=heads)
    clip_grad_norm_(body + heads, max_norm=1.0)
    opt.step()
  1. 1One task’s body gradient gi, line 1 of the objective in Section 3, as one row of G.
  2. 2Writes a flat vector into the .grad of body, which box 5 of Figure 13 reads.
  3. 3One backward pass per task, box 2 of Figure 13, stacked into G.
  4. 4GG and w*, the solver of Section 4.
  5. 5d*, the update of Section 5.
  6. 6d* to the body, the average loss’s gradient to the heads, clip, step (Figure 13).

Global-norm clipping rescales all gradients together when their combined length exceeds max_norm, keeping their direction. A larger max_norm clips less often, a huge one never. 1.0 is a common value, not from the CAGrad paper. Gradient Balancing Inside the Training Loop compares other clipping types.

The official CAGrad repository holds the paper’s code; LibMTL, an open-source PyTorch multitask library, ships it as --weighting CAGrad.

13 Comparison with other multitask methods

Reweight the losses when task gradients differ in size; edit the direction when they conflict.

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)
PCGradHow 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?
CAGrad (this page)How 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?

14 References

  1. Liu, B., et al. (2021). Conflict-Averse Gradient Descent for Multi-task Learning. NeurIPS. arXiv:2110.14048.