Skip to content
This repository was archived by the owner on Nov 17, 2025. It is now read-only.

Commit 56c9269

Browse files
Conditionally sum broadcasted dimensions in Elemwise and BroadcastTo gradients
1 parent 8cfbbd9 commit 56c9269

4 files changed

Lines changed: 85 additions & 28 deletions

File tree

aesara/tensor/elemwise.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,6 @@ def connection_pattern(self, node):
543543
return [[True for output in node.outputs] for ipt in node.inputs]
544544

545545
def L_op(self, inputs, outs, ograds):
546-
from aesara.tensor.math import sum as at_sum
547546

548547
# Compute grad with respect to broadcasted input
549548
rval = self._bgrad(inputs, outs, ograds)
@@ -574,18 +573,9 @@ def L_op(self, inputs, outs, ograds):
574573
if isinstance(rval[i].type, (NullType, DisconnectedType)):
575574
continue
576575

577-
# List of all the dimensions that are broadcastable for input[i] so
578-
# we can sum over them
579-
# TODO: only count dimensions that were effectively broadcasted
580-
to_sum = [
581-
j
582-
for j, bcast in enumerate(ipt.type.broadcastable)
583-
if bcast and not outs[0].broadcastable[j]
584-
]
585-
586-
if to_sum:
587-
sr = at_sum(rval[i], axis=to_sum, keepdims=True)
588-
rval[i] = sr
576+
rval[i] = aesara.tensor.extra_ops.sum_broadcasted_dims(
577+
rval[i], ipt, outs[0].type.shape
578+
)
589579

590580
return rval
591581

aesara/tensor/extra_ops.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections.abc import Collection
22
from functools import reduce
3-
from typing import Iterable, Set, Tuple, Union
3+
from typing import Iterable, Optional, Sequence, Set, Tuple, Union
44

55
import numpy as np
66
import numpy.core.numeric
@@ -1669,19 +1669,11 @@ def grad(self, inputs, outputs_gradients):
16691669

16701670
d_wrt_a = broadcast_to(dout, shape).sum(axis=new_dims)
16711671

1672-
# Determine the dimensions that were broadcast
1673-
_, static_shape = at.infer_static_shape(shape)
1674-
1675-
# TODO: This needs to be performed at run-time when static shape
1676-
# information isn't available.
1677-
bcast_sums = [
1678-
i
1679-
for i, (a_s, s_s) in enumerate(zip(a.type.shape, static_shape[-a.ndim :]))
1680-
if a_s == 1 and s_s != 1
1681-
]
1682-
1683-
if bcast_sums:
1684-
d_wrt_a = d_wrt_a.sum(axis=bcast_sums, keepdims=True)
1672+
# Determine the dimensions that were broadcast and sum them
1673+
static_out_shape = tuple(
1674+
s.data if isinstance(s, Constant) else None for s in shape[-a.ndim :]
1675+
)
1676+
d_wrt_a = sum_broadcasted_dims(d_wrt_a, a, static_out_shape)
16851677

16861678
return [d_wrt_a] + [
16871679
grad_undefined(self, i, shp) for i, shp in enumerate(shape, 1)
@@ -1808,6 +1800,46 @@ def broadcast_arrays(*args: TensorVariable) -> Tuple[TensorVariable, ...]:
18081800
return tuple(broadcast_to(a, broadcast_shape(*args)) for a in args)
18091801

18101802

1803+
def sum_broadcasted_dims(
1804+
value: TensorVariable,
1805+
inp: TensorVariable,
1806+
out_shape: Sequence[Optional[int]],
1807+
) -> TensorVariable:
1808+
"""Sum dimensions in `value` that are broadcasted between `inp`'s shape and `out_shape`.
1809+
1810+
For ambiguous cases, this builds a graph that determine whether or not
1811+
dimensions are to be summed at run-time.
1812+
1813+
"""
1814+
dims_to_sum = ()
1815+
ambiguous_dim_conds = ()
1816+
1817+
in_shape = inp.type.shape
1818+
1819+
for i, (s1, s2) in enumerate(zip(in_shape, out_shape)):
1820+
if s1 == 1 and s2 != 1:
1821+
dims_to_sum += (i,)
1822+
elif s1 is None and s2 != 1:
1823+
ambiguous_dim_conds += (
1824+
(i, aes.eq(at.scalar_from_tensor(inp.shape[i]), 1)),
1825+
)
1826+
1827+
if dims_to_sum:
1828+
value = at_sum(value, axis=dims_to_sum, keepdims=True)
1829+
1830+
if ambiguous_dim_conds:
1831+
from aesara.ifelse import ifelse
1832+
1833+
for i, cond in ambiguous_dim_conds:
1834+
value = ifelse(
1835+
cond,
1836+
at_sum(value, axis=i, keepdims=True),
1837+
value,
1838+
)
1839+
1840+
return value
1841+
1842+
18111843
__all__ = [
18121844
"searchsorted",
18131845
"cumsum",

tests/tensor/test_elemwise.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99
import aesara
1010
import aesara.scalar as aes
11+
import aesara.tensor as at
1112
import tests.unittest_tools as utt
12-
from aesara.compile.mode import Mode
13+
from aesara.compile.mode import Mode, get_default_mode
1314
from aesara.configdefaults import config
1415
from aesara.graph.basic import Apply, Variable
1516
from aesara.graph.fg import FunctionGraph
@@ -889,6 +890,39 @@ def test_invalid_static_shape(self):
889890
):
890891
x + y
891892

893+
def test_grad_sum_bcast_input_dims(self):
894+
"""Make sure broadcasted dimensions in the gradients are summed when static shape information isn't available."""
895+
Y = matrix("Y")
896+
X = matrix("X")
897+
X_grad = aesara.grad((X + Y).sum(), wrt=X)
898+
899+
mode = get_default_mode().including("fast_run")
900+
901+
X_grad_fn = aesara.function([X, Y], X_grad, mode=mode)
902+
res = X_grad_fn(np.ones((1, 5)), np.ones((5, 5)))
903+
assert np.array_equal(res, np.array([[5.0, 5.0, 5.0, 5.0, 5.0]]))
904+
905+
# When the shapes are known at compile-time, the compiled graph should
906+
# simplify
907+
Y = tensor(np.float64, shape=(5, None), name="Y")
908+
X = tensor(np.float64, shape=(1, 5), name="X")
909+
X_grad = aesara.grad((X + Y).sum(), wrt=X)
910+
911+
X_grad_fn = aesara.function([X, Y], X_grad, mode=mode)
912+
res = X_grad_fn(np.ones((1, 5)), np.ones((5, 5)))
913+
assert np.array_equal(res, np.array([[5.0, 5.0, 5.0, 5.0, 5.0]]))
914+
915+
assert X_grad_fn.maker.fgraph.apply_nodes
916+
917+
def test_grad_of_grad(self):
918+
"""This tests a special case in which the static shapes of a `DimShuffle` and its gradient don't match."""
919+
a = at.vector("a")
920+
921+
out = aesara.grad((a * a).sum(), a).sum()
922+
out = aesara.grad(out, a)
923+
924+
assert out.type.shape == (None,)
925+
892926

893927
def test_not_implemented_elemwise_grad():
894928
# Regression test for unimplemented gradient in an Elemwise Op.

tests/tensor/test_extra_ops.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,7 @@ def test_memory_leak(self):
13181318
[
13191319
[lambda x: broadcast_to(x, (1,)), (1,)],
13201320
[lambda x: broadcast_to(x, (6, 2, 5, 3)), (1,)],
1321+
[lambda x: broadcast_to(x, (6, 2, 5, 3)), (1,)],
13211322
[lambda x: broadcast_to(x, (6, 2, 5, 3)), (5, 1)],
13221323
[lambda x: broadcast_to(x, (6, 2, 1, 3)), (2, 1, 3)],
13231324
],

0 commit comments

Comments
 (0)