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

Commit 99f08ee

Browse files
Merge pull request #161 from brandonwillard/fix-fusion-c-code-condition
Avoid Elemwise fusion for scalar Ops without C implementations
2 parents 0150ddf + 2de5415 commit 99f08ee

2 files changed

Lines changed: 103 additions & 67 deletions

File tree

tests/tensor/test_opt.py

Lines changed: 96 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,7 +1202,16 @@ def test_cast_in_mul_canonizer():
12021202

12031203

12041204
class TestFusion:
1205-
mode = copy.copy(compile.mode.get_default_mode())
1205+
opts = theano.gof.Query(
1206+
include=[
1207+
"local_elemwise_fusion",
1208+
"composite_elemwise_fusion",
1209+
"canonicalize",
1210+
"inplace",
1211+
],
1212+
exclude=["cxx_only", "BlasOpt"],
1213+
)
1214+
mode = theano.compile.Mode(compile.mode.get_default_mode().linker, opts)
12061215
_shared = staticmethod(shared)
12071216
topo_exclude = ()
12081217

@@ -1879,10 +1888,6 @@ def my_init(shp, dtype="float64", num=0):
18791888
atol = 1e-6
18801889
if not np.allclose(out, answer * nb_repeat, atol=atol):
18811890
fail1.append(id)
1882-
print("cases", id)
1883-
print(val_inputs)
1884-
print(out)
1885-
print(answer * nb_repeat)
18861891
topo = f.maker.fgraph.toposort()
18871892
topo_ = [n for n in topo if not isinstance(n.op, self.topo_exclude)]
18881893
if assert_len_topo:
@@ -1905,52 +1910,39 @@ def my_init(shp, dtype="float64", num=0):
19051910
if not out_dtype == out.dtype:
19061911
fail4.append((id, out_dtype, out.dtype))
19071912

1908-
failed = len(fail1 + fail2 + fail3 + fail4)
1909-
if failed > 0:
1910-
print("Executed", len(cases), "cases", "failed", failed)
1911-
raise Exception("Failed %d cases" % failed, fail1, fail2, fail3, fail4)
1913+
assert len(fail1 + fail2 + fail3 + fail4) == 0
19121914

19131915
return times
19141916

19151917
def test_elemwise_fusion(self):
19161918
shp = (5, 5)
1917-
mode = copy.copy(self.mode)
1918-
# we need the optimisation enabled and the canonicalize.
1919-
# the canonicalize is needed to merge multiplication/addition by constant.
1920-
mode._optimizer = mode._optimizer.including(
1921-
"local_elemwise_fusion", "composite_elemwise_fusion", "canonicalize"
1922-
)
1923-
self.do(mode, self._shared, shp)
1919+
self.do(self.mode, self._shared, shp)
19241920

1925-
@pytest.mark.slow
1926-
def test_elemwise_fusion_4d(self):
1927-
shp = (3, 3, 3, 3)
1928-
mode = copy.copy(self.mode)
1929-
# we need the optimisation enabled and the canonicalize.
1930-
# the canonicalize is needed to merge multiplication/addition by constant.
1931-
mode._optimizer = mode._optimizer.including(
1932-
"local_elemwise_fusion", "composite_elemwise_fusion", "canonicalize"
1933-
)
1934-
self.do(mode, self._shared, shp, slice=slice(0, 1))
1935-
1936-
def test_fusion_35inputs(self):
1937-
# Make sure a fused graph with more than 35 inputs does not segfault
1938-
# or error.
1921+
def test_fusion_35_inputs(self):
1922+
"""Make sure we don't fuse too many `Op`s and go past the 31 function arguments limit."""
19391923
inpts = vectors(["i%i" % i for i in range(35)])
1924+
19401925
# Make an elemwise graph looking like:
19411926
# sin(i34 + sin(i33 + sin(... i1 + sin(i0) ...)))
19421927
out = tt.sin(inpts[0])
19431928
for idx in range(1, 35):
19441929
out = tt.sin(inpts[idx] + out)
19451930

1946-
f = function(inpts, out, mode=self.mode)
1947-
# Test it on some dummy values
1948-
f(*[list(range(i, 4 + i)) for i in range(35)])
1931+
with theano.change_flags(cxx=""):
1932+
f = function(inpts, out, mode=self.mode)
1933+
1934+
# Make sure they all weren't fused
1935+
composite_nodes = [
1936+
node
1937+
for node in f.maker.fgraph.toposort()
1938+
if isinstance(getattr(node.op, "scalar_op", None), scal.basic.Composite)
1939+
]
1940+
assert not any(len(node.inputs) > 31 for node in composite_nodes)
19491941

19501942
@pytest.mark.skipif(not theano.config.cxx, reason="No cxx compiler")
1951-
def test_pickle_big_fusion(self):
1943+
def test_big_fusion(self):
19521944
# In the past, pickle of Composite generated in that case
1953-
# crashed with max recusion limit. So we where not able to
1945+
# crashed with max recursion limit. So we were not able to
19541946
# generate C code in that case.
19551947
factors = []
19561948
sd = tt.dscalar()
@@ -1974,8 +1966,47 @@ def test_pickle_big_fusion(self):
19741966
logp = tt.add(*factors)
19751967

19761968
vars = [sd, means]
1977-
dlogp = function(vars, [theano.grad(logp, v) for v in vars])
1978-
dlogp(2, np.random.rand(n))
1969+
1970+
# Make sure that C compilation is used
1971+
mode = theano.compile.Mode("cvm", self.opts)
1972+
dlogp = function(vars, [theano.grad(logp, v) for v in vars], mode=mode)
1973+
1974+
# Make sure something was fused
1975+
assert any(
1976+
isinstance(getattr(node.op, "scalar_op", None), scal.basic.Composite)
1977+
for node in dlogp.maker.fgraph.toposort()
1978+
)
1979+
1980+
def test_add_mul_fusion_inplace(self):
1981+
1982+
opts = theano.gof.Query(
1983+
include=[
1984+
"local_elemwise_fusion",
1985+
"composite_elemwise_fusion",
1986+
"canonicalize",
1987+
"inplace",
1988+
],
1989+
exclude=["cxx_only", "BlasOpt"],
1990+
)
1991+
1992+
mode = theano.compile.mode.Mode(self.mode.linker, opts)
1993+
1994+
x, y, z = dmatrices("xyz")
1995+
out = tt.dot(x, y) + x + y + z
1996+
f = function([x, y, z], out, mode=mode)
1997+
topo = [n for n in f.maker.fgraph.toposort()]
1998+
assert len(topo) == 2
1999+
assert topo[-1].op.inplace_pattern
2000+
2001+
new_out = f.maker.fgraph.outputs[0]
2002+
assert isinstance(new_out.owner.op, Elemwise)
2003+
assert isinstance(new_out.owner.op.scalar_op, scal.basic.Add)
2004+
assert len(new_out.owner.inputs) == 4
2005+
2006+
# TODO: Do we really need to do this?
2007+
_ = f(
2008+
np.random.random((5, 5)), np.random.random((5, 5)), np.random.random((5, 5))
2009+
)
19792010

19802011
def speed_fusion(self, s=None):
19812012
"""
@@ -2035,28 +2066,6 @@ def speed_fusion(self, s=None):
20352066
d.std(),
20362067
)
20372068

2038-
def test_fusion_inplace(self):
2039-
mode = copy.copy(self.mode)
2040-
# we need the optimisation enabled and the canonicalize.
2041-
# the canonicalize is needed to merge multiplication/addition by constant.
2042-
mode._optimizer = mode._optimizer.including(
2043-
"local_elemwise_fusion",
2044-
"composite_elemwise_fusion",
2045-
"canonicalize",
2046-
"inplace",
2047-
)
2048-
2049-
x, y, z = dmatrices("xyz")
2050-
f = function([x, y, z], tt.dot(x, y) + x + y + z, mode=mode)
2051-
topo = [
2052-
n
2053-
for n in f.maker.fgraph.toposort()
2054-
if not isinstance(n.op, self.topo_exclude)
2055-
]
2056-
assert len(topo) == 2
2057-
assert topo[-1].op.inplace_pattern
2058-
f(np.random.random((5, 5)), np.random.random((5, 5)), np.random.random((5, 5)))
2059-
20602069
def speed_log_exp(self):
20612070
s = slice(31, 36)
20622071
print(
@@ -2071,6 +2080,34 @@ def speed_log_exp(self):
20712080
),
20722081
)
20732082

2083+
@pytest.mark.skipif(not theano.config.cxx, reason="No cxx compiler")
2084+
def test_no_c_code(self):
2085+
"""Make sure we avoid fusions for `Op`s without C code implementations."""
2086+
2087+
# This custom `Op` has no `c_code` method
2088+
class NoCCodeOp(scal.basic.UnaryScalarOp):
2089+
def impl(self, x):
2090+
return x * 2
2091+
2092+
no_c_code_op = Elemwise(NoCCodeOp(scal.basic.upgrade_to_float))
2093+
2094+
mode = theano.Mode(linker="cvm")
2095+
mode._optimizer = mode._optimizer.including(
2096+
"local_elemwise_fusion",
2097+
"composite_elemwise_fusion",
2098+
"canonicalize",
2099+
"inplace",
2100+
)
2101+
2102+
x = tt.vector()
2103+
out = x * no_c_code_op(x + 1)
2104+
f = function([x], out, mode=mode)
2105+
2106+
assert not any(
2107+
isinstance(getattr(n.op, "scalar_op"), scal.basic.Composite)
2108+
for n in f.maker.fgraph.toposort()
2109+
)
2110+
20742111

20752112
class TimesN(scal.basic.UnaryScalarOp):
20762113
"""

theano/tensor/opt.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7624,11 +7624,10 @@ def local_fuse(node):
76247624
except (NotImplementedError, MethodNotDefined):
76257625
_logger.warning(
76267626
(
7627-
"%s does not implement the c_code function."
7628-
" As well as being potentially slow, this"
7629-
" disables loop fusion of this op."
7627+
f"The Op {i.owner.op.scalar_op} does not provide a C implementation."
7628+
" As well as being potentially slow, this also disables "
7629+
"loop fusion."
76307630
)
7631-
% str(i.owner.op.scalar_op)
76327631
)
76337632
do_fusion = False
76347633

@@ -7693,12 +7692,12 @@ def local_fuse(node):
76937692
except (NotImplementedError, MethodNotDefined):
76947693
_logger.warning(
76957694
(
7696-
"%s does not implement the c_code function."
7697-
" As well as being potentially slow, this disables "
7698-
"loop fusion of this op."
7695+
f"The Op {s_new_out[0].owner.op} does not provide a C implementation."
7696+
" As well as being potentially slow, this also disables "
7697+
"loop fusion."
76997698
)
7700-
% str(s_new_out[0].owner.op)
77017699
)
7700+
return False
77027701

77037702
# create the composite op.
77047703
composite_op = ts.Composite(s_inputs, s_new_out)

0 commit comments

Comments
 (0)