The backward call will follow all computation graphs and update the parameters accordingly.
If a1, a2 etc. were computed using the same model and thus the same parameters, the gradient will be accumulated in them.
Let’s have a look at my dummy model::
# loss = loss1
torch.manual_seed(2809)
model = MyModel()
criterion = nn.MSELoss()
x = torch.randn(1, 10)
target = torch.randn(1, 10)

out1, out2 = model(x)
loss1 = criterion(out1, target)
loss2 = criterion(out2, target)
loss = loss1
loss.backward()
grad1 = model.fc1.weight.grad.clone()

# loss = loss2
torch.manual_seed(2809)
model = MyModel()
criterion = nn.MSELoss()
x = torch.randn(1, 10)
target = torch.randn(1, 10)

out1, out2 = model(x)
loss1 = criterion(out1, target)
loss2 = criterion(out2, target)
loss = loss2
loss.backward()
grad2 = model.fc1.weight.grad.clone()

# loss = loss1 + loss2
torch.manual_seed(2809)
model = MyModel()
criterion = nn.MSELoss()
x = torch.randn(1, 10)
target = torch.randn(1, 10)

out1, out2 = model(x)
loss1 = criterion(out1, target)
loss2 = criterion(out2, target)
loss = loss1 + loss2
loss.backward()
grad3 = model.fc1.weight.grad.clone()

torch.allclose(grad1 + grad2, grad3)
> True

As you can see, the backward calls on each loss separately yield the same result as a sum as the backward call on the summed losses.
If you check the grad_fn of loss, you’ll see multiple paths:
print(loss.grad_fn.next_functions)
> ((<MseLossBackward at 0x7f8cd85b0c18>, 0),
 (<MseLossBackward at 0x7f8cd85b0ba8>, 0))