Skip to content

Commit 8bb24bc

Browse files
committed
Fix import performance regression and update time limits
Unfortunately, #3030 doubled the import time for our performance test model, but the check there was too lax to catch it. Sometimes, `smart_subs_dict` is applied to individual expressions, sometimes to complete matrices. If it's only applied to individual expressions or small matrices, avoid the overhead of flattening the substitutions first. Update the time limits for the performance tests to catch such regressions earlier. Fixes #3048.
1 parent baed9cd commit 8bb24bc

File tree

3 files changed

+73
-28
lines changed

3 files changed

+73
-28
lines changed

python/sdist/amici/importers/utils.py

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ def smart_subs_dict(
400400
subs: SymbolDef,
401401
field: str | None = None,
402402
reverse: bool = True,
403+
flatten_first: bool | None = None,
403404
) -> sp.Expr:
404405
"""
405406
Substitutes expressions completely flattening them out. Requires
@@ -418,6 +419,11 @@ def smart_subs_dict(
418419
Whether ordering in subs should be reversed. Note that substitution
419420
requires the reverse order of what is required for evaluation.
420421
422+
:param flatten_first:
423+
Choice of algorithm: Flatten the substitution expressions first, then
424+
substitute them simultaneously into `sym` (``True``), or substitute
425+
them one by one into `sym` (``False``).
426+
421427
:return:
422428
Substituted symbolic expression
423429
"""
@@ -426,27 +432,61 @@ def smart_subs_dict(
426432
else:
427433
s = [(eid, expr[field]) for eid, expr in subs.items()]
428434

429-
if not reverse:
430-
# counter-intuitive, but we need to reverse the order for reverse=False
431-
s.reverse()
432-
433-
with sp.evaluate(False):
434-
# The new expressions may themselves contain symbols to be substituted.
435-
# We flatten them out first, so that the substitutions in `sym` can be
436-
# performed simultaneously, which is usually more efficient than
437-
# repeatedly substituting into `sym`.
438-
# TODO(performance): This could probably be made more efficient by
439-
# combining with toposort used to order `subs` in the first place.
440-
# Some substitutions could be combined, and some terms not present in
441-
# `sym` could be skipped.
442-
for i in range(len(s) - 1):
443-
for j in range(i + 1, len(s)):
444-
if s[j][1].has(s[i][0]):
445-
s[j] = s[j][0], s[j][1].xreplace({s[i][0]: s[i][1]})
446-
447-
s = dict(s)
448-
sym = sym.xreplace(s)
449-
return sym
435+
# We have the choice to flatten the replacement expressions first or to
436+
# substitute them one by one into `sym`. Flattening first is usually
437+
# more efficient if `sym` is large (e.g., a matrix with many elements)
438+
# and `subs` is cascading (i.e., substitutions depend on other
439+
# substitutions). Otherwise, substituting one by one is usually more
440+
# efficient, because flattening scales quadratically with the number of
441+
# substitutions.
442+
# The exact threshold is somewhat arbitrary and may need to be
443+
# adjusted in the future.
444+
if flatten_first is None:
445+
flatten_first = (
446+
isinstance(sym, sp.MatrixBase) and sym.rows * sym.cols > 20
447+
)
448+
449+
if flatten_first:
450+
if not reverse:
451+
# counter-intuitive, but on this branch, we need to reverse the
452+
# order for `reverse=False`
453+
s.reverse()
454+
455+
with sp.evaluate(False):
456+
# The new expressions may themselves contain symbols to be
457+
# substituted. We flatten them out first, so that the
458+
# substitutions in `sym` can be performed simultaneously,
459+
# which can be more efficient than repeatedly substituting into
460+
# `sym`.
461+
# TODO(performance): This could probably be made more efficient by
462+
# combining with toposort used to order `subs` in the first
463+
# place.
464+
# Some substitutions could be combined, and some terms not
465+
# present in `sym` could be skipped.
466+
# Furthermore, this would provide information on recursion depth,
467+
# which might help decide which strategy is more efficient.
468+
# For flat hierarchies, substituting one by one is most likely
469+
# more efficient.
470+
for i in range(len(s) - 1):
471+
for j in range(i + 1, len(s)):
472+
if s[j][1].has(s[i][0]):
473+
s[j] = s[j][0], s[j][1].xreplace({s[i][0]: s[i][1]})
474+
475+
s = dict(s)
476+
sym = sym.xreplace(s)
477+
return sym
478+
479+
else:
480+
if reverse:
481+
s.reverse()
482+
483+
with sp.evaluate(False):
484+
for old, new in s:
485+
# note that substitution may change free symbols,
486+
# so we have to do this recursively
487+
if sym.has(old):
488+
sym = sym.xreplace({old: new})
489+
return sym
450490

451491

452492
def smart_subs(element: sp.Expr, old: sp.Symbol, new: sp.Expr) -> sp.Expr:

python/tests/test_misc.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def test_cmake_compilation(sbml_example_presimulation_module):
8484

8585

8686
@skip_on_valgrind
87-
def test_smart_subs_dict():
87+
@pytest.mark.parametrize("flatten_first", [True, False])
88+
def test_smart_subs_dict(flatten_first):
8889
expr_str = "c + d"
8990
subs_dict = {
9091
"c": "a + b",
@@ -98,8 +99,12 @@ def test_smart_subs_dict():
9899
expected_default = sp.sympify(expected_default_str)
99100
expected_reverse = sp.sympify(expected_reverse_str)
100101

101-
result_default = smart_subs_dict(expr_sym, subs_sym)
102-
result_reverse = smart_subs_dict(expr_sym, subs_sym, reverse=False)
102+
result_default = smart_subs_dict(
103+
expr_sym, subs_sym, flatten_first=flatten_first
104+
)
105+
result_reverse = smart_subs_dict(
106+
expr_sym, subs_sym, reverse=False, flatten_first=flatten_first
107+
)
103108

104109
assert sp.simplify(result_default - expected_default).is_zero
105110
assert sp.simplify(result_reverse - expected_reverse).is_zero

tests/performance/reference.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Reference wall times (seconds) with some buffer
22
create_sdist: 16
33
install_sdist: 150
4-
petab_import: 2100
5-
install_model: 120
4+
petab_import: 720
5+
install_model: 60
66
install_model_O0: 40
7-
install_model_O1: 90
8-
install_model_O2: 120
7+
install_model_O1: 45
8+
install_model_O2: 60
99
forward_simulation: 2
1010
forward_sensitivities: 2
1111
adjoint_sensitivities: 2.5

0 commit comments

Comments
 (0)