I can add two tensors x and y inplace like this
x = x.add(y)
Is there a way of doing the same with three or more tensors given all tensors have same dimensions?
I can add two tensors x and y inplace like this
x = x.add(y)
Is there a way of doing the same with three or more tensors given all tensors have same dimensions?
result = torch.sum(torch.stack([x, y, ...]), dim=0)
Without stack:
from functools import reduce
result = reduce(torch.add, [x, y, ...])
EDIT
As @LudvigH pointed out, the second method is not as memory-efficient, as inplace addition. So it's better like this:
from functools import reduce
result = reduce(
torch.Tensor.add_,
[x, y, ...],
torch.zeros_like(x) # optionally set initial element to avoid changing `x`
)
How important is it that the operations occur in place?
I believe the only way to do addition in place is with the add_ function.
For example:
a = torch.randn(5)
b = torch.randn(5)
c = torch.randn(5)
d = torch.randn(5)
a.add_(b).add_(c).add_(d) # in place addition of a+b+c+d
In general, in-place operations in PyTorch is tricky. They discourage using it. I think it stems from the fact that it is easy to mess up, ruinig the computation graph and giving you unexpected results. Also, there are many GPU optimizations that will be done anyway, and forcing in place operations can slow down your performance in the end. But assuming that your really know what you are doing, and you want to sum a lot of tensors with compatible shapes I would use the following pattern:
import functools
import operator
list_of_tensors = [a, b, c] # some tensors previously defined
functools.reduce(operator.iadd, list_of_tensors)
### now tensor a in the in-place sum of all the tensors
It builds on the pattern of reduce, which means "do this to all elements in the list/iterable", and operator.iadd which means +=. There are many caveats with +=, since it may mess up with scoping and behaves unexpectedly with immutable variables such as strings. But in the context of PyTorch, it does what we want. It makes calls to add_.
Below, you can see a simple benchmark.
from functools import reduce
from operator import iadd
import torch
def make_tensors():
return [torch.randn(5, 5) for _ in range(1000)]
def profile_action(label, action):
print(label)
list_of_tensors = make_tensors()
with torch.autograd.profiler.profile(
profile_memory=True, record_shapes=True
) as prof:
action(list_of_tensors)
print(prof.key_averages().table(sort_by="self_cpu_memory_usage"))
profile_action("Case A:", lambda tensors: torch.sum(torch.stack(tensors), dim=0))
profile_action("Case B:", lambda tensors: sum(tensors))
profile_action("Case C:", lambda tensors: reduce(torch.add, tensors))
profile_action("Case C:", lambda tensors: reduce(iadd, tensors))
The results vary between runs of course, bit this copy-paste was somewhat representative on my machine. Try it on yours! It probably changes a bit with pytorch version as well...
-------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Name Self CPU % Self CPU CPU total % CPU total CPU time avg CPU Mem Self CPU Mem # of Calls
-------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
aten::resize_ 0.14% 14.200us 0.14% 14.200us 14.200us 97.66 Kb 97.66 Kb 1
aten::empty 0.06% 5.800us 0.06% 5.800us 2.900us 100 b 100 b 2
aten::stack 17.38% 1.751ms 98.71% 9.945ms 9.945ms 97.66 Kb 0 b 1
aten::unsqueeze 30.55% 3.078ms 78.55% 7.914ms 7.914us 0 b 0 b 1000
aten::as_strided 48.02% 4.837ms 48.02% 4.837ms 4.833us 0 b 0 b 1001
aten::cat 0.73% 73.800us 2.78% 280.000us 280.000us 97.66 Kb 0 b 1
aten::_cat 1.87% 188.900us 2.05% 206.200us 206.200us 97.66 Kb 0 b 1
aten::sum 1.09% 109.400us 1.29% 130.100us 130.100us 100 b 0 b 1
aten::fill_ 0.17% 16.700us 0.17% 16.700us 16.700us 0 b 0 b 1
[memory] 0.00% 0.000us 0.00% 0.000us 0.000us -97.75 Kb -97.75 Kb 2
-------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Self CPU time total: 10.075ms
----------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Name Self CPU % Self CPU CPU total % CPU total CPU time avg CPU Mem Self CPU Mem # of Calls
----------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
aten::add 99.32% 14.711ms 100.00% 14.812ms 14.812us 97.66 Kb 97.65 Kb 1000
aten::empty_strided 0.07% 10.400us 0.07% 10.400us 10.400us 4 b 4 b 1
aten::to 0.37% 54.900us 0.68% 100.400us 100.400us 4 b 0 b 1
aten::copy_ 0.24% 35.100us 0.24% 35.100us 35.100us 0 b 0 b 1
[memory] 0.00% 0.000us 0.00% 0.000us 0.000us -97.66 Kb -97.66 Kb 1002
----------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Self CPU time total: 14.812ms
------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Name Self CPU % Self CPU CPU total % CPU total CPU time avg CPU Mem Self CPU Mem # of Calls
------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
aten::add 100.00% 10.968ms 100.00% 10.968ms 10.979us 97.56 Kb 97.56 Kb 999
[memory] 0.00% 0.000us 0.00% 0.000us 0.000us -97.56 Kb -97.56 Kb 999
------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Self CPU time total: 10.968ms
-------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Name Self CPU % Self CPU CPU total % CPU total CPU time avg CPU Mem Self CPU Mem # of Calls
-------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
aten::add_ 100.00% 5.184ms 100.00% 5.184ms 5.190us 0 b 0 b 999
-------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Self CPU time total: 5.184ms
I allocate 1000 tensors holding 25 times 32 bit floats (total 100b per tensor, 100kb=97.66Kb in total). The difference in runtime and memory footprint is quite stunning.
Case A, torch.sum(torch.stack(list_of_tensors), dim=0) allocates 100kb for the stack, and 100b for the result, taking 10 ms.
Case B sum takes 14ms. Mostly because of python overhead I guess. It allocates 10kb for all intermediary results of each addition.
Case C uses reduce-add which gets rid of some overhead, gaining in runtime performance (11ms) but still allocating intermediary results. This time, it does not start with a 0-initialization, which sum does, so we only do 999 additions instead of 1000, and allocate one intermediate result less. The difference with Case B is minute, and in most runs they had the same runtime.
Case D is my recommended way for in place addition of an iterable/list of tensors. It takes roughly half the time and allocates no extra memory. Efficient. But you waste the first tensor in the list, since you perform the operation in place.