← All tutorials

GradVac (Gradient Vaccine)

GradVac keeps each pair of task gradients about as aligned as that pair usually is.

On this page

1 PCGrad’s fixed target of zero 2 Gradient similarity in multilingual translation 3 A running target per task pair 4 The edit toward the target 5 One GradVac step 6 Results on translation and tagging 7 Implementation in PyTorch 8 Comparison with other methods 9 References

GradVac (Gradient Vaccine; Wang et al., 2021) edits the task gradients before they are added into one update. When two tasks’ gradients agree less than usual, it tilts one toward the other. Agreement is the cosine similarity, the dot product divided by both lengths: 1 for the same direction, 0 at a right angle, −1 for opposite. Each task pair keeps a target cosine, the running average of its past cosines. Below the target, GradVac adds the coefficient a times one gradient to the other, with a chosen so the cosine equals the target.

GradVac runs every training step on that step’s minibatch and costs one backward pass per task, K for K tasks. Its state is one target per task pair per parameter group, a block of shared weights; one layer per group was best in the paper.

1 PCGrad’s fixed target of zero

PCGrad (projecting conflicting gradients) edits a pair only when its cosine is negative, projecting one gradient so the cosine becomes 0: every pair has target 0. A pair that usually sits at 0.6 can drop to 0.2, and PCGrad does nothing. GradVac gives each pair its own target, so it acts here too; with every target at 0 it is exactly PCGrad. Like a vaccine, it acts before the cosine turns negative.

1 Positive cosine, below targetits targetGradVacPCGrad leaves it2 Negative cosine (conflict)its targetGradVacPCGrad−10+1cosine between the pair's gradients
Figure 1. GradVac lifts both pairs to their targets; PCGrad lifts only pair 2, to 0.
Figure 5 case a of the GradVac paper: PCGrad and GradVac on negative gradient cosine
Figure 5 case a of the GradVac paper, PCGrad's panel: negative gradient cosine
Figure 5 case a of the GradVac paper, GradVac's panel: negative gradient cosine

(a) Negative cosine. PCGrad lefttop, GradVac rightbottom.

Figure 5 case b of the GradVac paper: PCGrad and GradVac on positive gradient cosine
Figure 5 case b of the GradVac paper, PCGrad's panel: positive gradient cosine
Figure 5 case b of the GradVac paper, GradVac's panel: positive gradient cosine

(b) Positive cosine below the target.

Figure 2. Paper’s Figure 5, arXiv:2010.05874; a2 is our a: “Comparing PCGrad (left) [top] with GradVac (right) [bottom] in two cases. …”

2 Gradient similarity in multilingual translation

The authors trained two translation models, one from every language into English and one from English into every language. Each language pair, such as English to French, is one task. Every 1000 steps they measured the cosine between every pair of task gradients, for analysis only.

ObservationWhat the paper found
Related languagesLanguages of one family have higher cosines and cluster.
Alignment and qualityTrained on English to French plus one other pair, a higher cosine went with better English to French.
Layers and training timeThe cosine differs by layer and over training.
Sign of the cosineMostly positive, so PCGrad’s projection acted on few steps.

Each pair has its own usual cosine; a fixed target of 0 ignores it.

3 A running target per task pair

For tasks i and j with gradients gi and gj (subscript ij marks the pair), GradVac tracks the pair’s usual cosine with an exponential moving average (EMA).

An illustrative pair of translation tasks, English to French and English to Hindi, at averaging rate 0.01. The walkthrough in Section 5 draws the same pair.

value
old target φij0.45
this batch’s cosine0.30
new target, 0.99 × 0.45 + 0.01 × 0.300.4485
GradVac remembers each task pair’s usual cosine as a slowly moving target. With β = 0.01 each update is 99 percent old target and 1 percent new cosine.
φij := (1−β) φij + β φij
The pair's running target φij becomes a blend of its old self and this batch's cosine φij, cos(gi, gj); the sign := means becomes. The hat marks the running estimate; no hat means one batch's measured value. The averaging rate β splits the weight: 1−β on the old target, β on the new cosine. Repeated every step, this gives the pair’s usual cosine rather than today’s.
So the target is what this pair usually looks like: one batch at 0.30 moves it from 0.45 only to 0.4485.

Raising β follows recent steps faster but lets one noisy batch move the target; lowering it averages over about the last 1/β steps. At β = 0 the target stays at its start, 0, and GradVac is PCGrad; at β = 1 it is the last batch’s cosine.

4 The edit toward the target

The same illustrative pair, with gi = (2.2, 0.6), batch cosine 0.30 and target 0.45.

value
lengths of gi and gj2.280 and 1.8
current and target angle72.5° and 63.3°
sine of their difference0.161
coefficient a0.229
cosine after the edit0.45
When this batch’s cosine falls below the target, GradVac adds just enough of gj to gi to lift the cosine to the target, then adds the gradients.
cos(gi, gj) < φij
The target is the running target of Section 3. GradVac edits the pair only when this batch’s cosine is below the running target φij. A pair at or above its target is left unchanged. The target used here is the one from before this batch; GradVac compares and edits first and only then folds this batch’s cosine into the target, so a low batch cannot lower its own threshold.
a = ‖gi‖‖gj‖ × φij sin θ − cos θ sin θsin θ
Adding a copies of gj turns gi exactly to the target angle. The double bars are lengths, θ is the current angle and θ the target angle (cos θ = φij), so the top is sin(θ − θ): zero at the target, and a target of 0 gives PCGrad’s projection.
gi′ = gi + a gj
The edited gradient gi′ is gi plus a times gj. Adding a piece along gj tilts gi toward it, which raises their cosine; it also changes gi's length. Everything else in gi is kept, so task i still pushes toward its own goal.
d = gi′ + gj
The combined gradient d is the edited gradient plus gj, and the step moves against it. Here only task i is edited, as in the paper's best setting, which edits only the low-resource languages (the ones with little training data). If task j is edited too, it gets its own edit toward gi and d adds the two edited gradients.
So GradVac aims for the pair’s usual agreement instead of only removing conflict. Here a = 0.229 lifts the cosine from 0.30 to the target 0.45. A target of 0 gives PCGrad’s cut, which fires only below a cosine of 0, so not on this pair at 0.30.

Figure 3 draws its own illustrative pair, with g1 as gi and g2 as gj.

g1′ (preview) g1 g2

Figure 3. A target of 0 gives PCGrad; one at or below the cosine gives no edit.

5 One GradVac step

the running target 0.45 past batches after this batch 0.4485 cosine vs target target 0.45 batch 0.30 gap 0.15

Figure 4. One pair, g1 as gi and g2 as gj, with only task 1 edited.

One GradVac step compares, edits gi, updates the target, then adds the gradients.
cos(gi, gj) < φij
This is the test of Section 4. This batch’s cosine says how aligned the pair is right now.Is this batch's cosine below the pair's running target φij, as it stood before this batch?
a = ‖gi‖‖gj‖ × φij sin θ − cos θ sin θsin θ
This line gives the coefficient a that lands the cosine exactly on the target; a pair with no gap gets no edit.
gi′ = gi + a gj
The edited gradient, drawn as the red arrow; the added part a×gj is the purple arrow.
φij := (1−β) φij + β φij
After the edit, the target moves a fraction β toward this batch’s cosine, measured before the edit, as in the update of Section 3.
d = gi′ + gj
The combined gradient d, the green arrow; the step moves against it.
So one step lifts this batch’s cosine from 0.30 to 0.45 and moves the pair’s usual cosine, its running target, from 0.45 to 0.4485.
gi task i's gradientgj task j's gradientcos(gi, gj) this batch’s cosinecos(gi, gj) this batch’s cosine, cos θφij this batch’s cosineφij the pair's running target cosinea the coefficient (purple a×g2)β the averaging rate, 0.01 heregi′ gi after the edit (red g1′)d the combined gradient (green)‖gi‖, ‖gj‖ the gradient lengthsθ the pair’s current angleθ the target angle, with cos θ = φij< less than:= becomes, updated to

6 Results on translation and tagging

Translation is scored in BLEU, the overlap with reference translations; tagging in F1, which balances precision and recall.

BenchmarkCompared withResult
WMT translation (the Workshop on Machine Translation data), 4 languages: French and Czech with much data, Hindi and Turkish with littlePlain model, GradNorm, MGDA, PCGradGradVac best on average and on every language; no other method beat the plain model on every language.
In-house translation, 25 languages crawled from the webPlain model, oversampling low-resource languages (the paper’s default)Largest gains on the high-resource languages.
Same dataPlain model, sampling by data sizeGains at every resource level.
Multilingual BERT, named-entity tagging (marking names of people, places and organizations) per languagePCGradAverage F1 83.8 against 83.1, not on every language.

The Field Guide lists the pairwise cosines among the values to log.

7 Implementation in PyTorch

Two listings, setup then method, make one GradVac step for a shared trunk with task heads. As in LibMTL, only the trunk is edited; the paper’s model shares every parameter.

import math
import random
import torch
from torch.autograd import grad

BETA = 0.01  # paper's best
EPS = 1e-8
KEEP = dict(retain_graph=True)
cos = torch.cosine_similarity

def flat_grad(loss, body):
    gs = grad(loss, body, **KEEP)
    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

kids = trunk.children()
layers = [m for m in kids if list(m.parameters())]
slices, i = [], 0
body = []
for m in layers:
    ps = list(m.parameters())
    n = sum(p.numel() for p in ps)
    slices.append(slice(i, i + n))
    i += n
    body += ps
C = len(slices)
head_ps = list(heads.parameters())
phi_hat = torch.zeros(K, K, C)
EDIT = range(K)
  1. 1Flat per-task trunk gradients and the write-back, as in Section 3 of Gradient Balancing Inside the Training Loop.
  2. 2One slice per layer (each trunk child with weights) and the targets φij per pair per layer, starting at 0.
def coef(g_i, g_j, phi, tgt):
    s = math.sqrt(1 - phi * phi)
    t = math.sqrt(1 - tgt * tgt)
    n_j = g_j.norm() + EPS
    r = g_i.norm() / n_j
    num = tgt * s - phi * t
    return r * num / (t + EPS)

def edit(g_i, g_j, ijk):
    phi = cos(g_i, g_j, 0).item()
    tgt = phi_hat[ijk].item()
    ema = (1 - BETA) * tgt
    new = ema + BETA * phi
    phi_hat[ijk] = new
    if phi >= tgt:
        return g_i
    a = coef(g_i, g_j, phi, tgt)
    return g_i + a * g_j

def edit_ij(out, G, i, j):
    for k in range(C):
        sl = slices[k]
        g_i = out[i, sl]
        g_j = G[j, sl]
        ijk = (i, j, k)
        new = edit(g_i, g_j, ijk)
        out[i, sl] = new

def gradvac(G, edit_tasks):
    out = G.clone()
    for i in edit_tasks:
        others = list(range(K))
        others.remove(i)
        random.shuffle(others)
        for j in others:
            edit_ij(out, G, i, j)
    return out.sum(0)

def gradvac_step(losses):
    rows = []
    for L in losses:
        g_k = flat_grad(L, body)
        rows.append(g_k)
    G = torch.stack(rows)
    g = gradvac(G, EDIT)
    opt.zero_grad()
    fill_grads(body, g)
    total = sum(losses)
    total.backward(inputs=head_ps)
    opt.step()
  1. 1The coefficient a, line 2 of the edit in Section 4.
  2. 2Target update (Section 3), then check and edit (Section 4, lines 1 and 3) against the old target, returning a new tensor.
  3. 3Task i against each layer of task j, always the unedited gj; the edited slice goes into out.
  4. 4Every other task in random order; the sum of the edited gradients is d, line 4.
  5. 5K backward passes, edit, write-back, heads’ plain gradients, optimizer step.
SettingPaper valueEffect
Averaging rate β0.01, best of 0.1, 0.01, 0.001About 100 steps of memory; raising and lowering as in Section 3.
Parameter groupsOne per layer, best of whole model, encoder and decoder, per layer, per weight matrixFiner groups track each layer’s cosine; “neither too coarse nor too fine-grained perform the best”.
Tasks editedLow-resource only, best of all, low-resource and high-resourceLeaves high-resource gradients untilted. EDIT in the listing.
Rescaling the edited gradientNone; “sufficient to ignore this step”The listing does not rescale.
Target start0Targets need about 100 steps to catch up, so early steps act like PCGrad.

No official code was found. LibMTL (github.com/median-research-group/LibMTL) defaults to β = 0.5 and the whole model as one group. Each step costs K backward passes, which dominate with many tasks, and K copies of the shared gradient; the K × K targets per layer are small.

  1. A target of 1 or −1, or an all-zero gj, sets the denominator in the formula for a to zero; the listing adds EPS to both.
  2. If logged cosines are often negative, PCGrad already acts on most steps; GradVac adds most when cosines stay positive but drop below usual.

7.1 Where GradVac sits in the training step

GradVac replaces loss.backward().

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 5. Where gradient methods act in one training step.

Momentum and Adam’s per-coordinate scaling can still turn the step (Sections 6 and 7.2 of Gradient Balancing Inside the Training Loop).

8 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).

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

GradVac makes no promise about the smallest per-task improvement; CAGrad raises it.

9 References

  1. Liu, B., et al. (2021). Conflict-Averse Gradient Descent for Multi-task Learning. NeurIPS. arXiv:2110.14048.
  2. Wang, Z., et al. (2021). Gradient Vaccine: Investigating and Improving Multi-task Optimization in Massively Multilingual Models. ICLR. arXiv:2010.05874.
  3. Yu, T., et al. (2020). Gradient Surgery for Multi-Task Learning. NeurIPS. arXiv:2001.06782.
  4. Lin, B., and Zhang, Y. (2023). LibMTL: A Python Library for Multi-Task Learning. JMLR. Code: github.com/median-research-group/LibMTL.