Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG] Caching and small run preparation improvements #832

Merged
merged 45 commits into from Sep 8, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
347620c
Make `CodeString`, `SingleEquation` and `Equations` comparable/hashable
mstimberg Apr 4, 2017
ea4e03f
Implement caching for state updates
mstimberg Apr 4, 2017
1d286df
Cache subexpression substitution in equations
mstimberg Apr 4, 2017
b714416
Cache `make_statements`
mstimberg Apr 4, 2017
12b1843
Use more conservative approach for caching regarding variables
mstimberg Apr 4, 2017
d4c3bdc
Small speed improvement for `sympy_to_str`
mstimberg Apr 5, 2017
c0e4850
Make `SympyNodeRenderer` emit sympy objects directly
mstimberg Apr 5, 2017
88dc2ad
Cache `sympy_to_str` and `str_to_sympy`
mstimberg Apr 5, 2017
011820d
Cache `CodeObject`
mstimberg Apr 5, 2017
a1bfddc
Translate variables to the namespace for every run (the same `CodeObj…
mstimberg Apr 5, 2017
fa200a9
Make hashable function more generic and avoid storing strong referenc…
mstimberg Apr 7, 2017
05c9544
Fix handling of True/False for sympy on Python 3
mstimberg Apr 7, 2017
6d16492
Use the `_hashable` function consistently
mstimberg Apr 7, 2017
a30371f
Store the variable references in a `CodeObject` as weak references
mstimberg Apr 7, 2017
83ac3e7
Empty the `CodeObject`'s namespace after every run
mstimberg Apr 7, 2017
fdd50de
Hold strong references for dynamic function implementations to save t…
mstimberg Apr 7, 2017
049a654
numpy: Translate `Function` objects to their implementation only when…
mstimberg Apr 7, 2017
49eef8c
Allow weak references to the Cython spike queue
mstimberg Apr 7, 2017
6803457
numpy's `rand`/`randn` functions cannot be weakly referenced directly…
mstimberg Apr 7, 2017
402de76
Compare to `None` directly instead of checking for `NoneType` (not co…
mstimberg Apr 7, 2017
5cff0ed
Revert commits related to `CodeObject` caching
mstimberg Apr 10, 2017
ca1dfb2
Cache `parse_statement`
mstimberg Apr 10, 2017
3b34b53
Cache `CodeGenerator.translate`
mstimberg Apr 10, 2017
fb5d6d7
Use a description of `Variable` objects to decide whether they are su…
mstimberg Apr 10, 2017
8633c7b
Cache `parse_string_equations`
mstimberg Apr 10, 2017
38b4360
Avoid changing the underlying `_equations` dictionary of an `Equation…
mstimberg Apr 10, 2017
eea3189
Take dependencies into account when caching code
mstimberg Apr 10, 2017
ffde29e
Deal with the function namespace when converting from variables to na…
mstimberg Apr 10, 2017
dc1378e
Avoid filling up the logs with errors from "make clean"
mstimberg Apr 11, 2017
b6f9e1e
Use all variables' owners instead of the dependencies (otherwise, ind…
mstimberg Apr 11, 2017
26c6229
Add the preferences to the cache key since they can influence the gen…
mstimberg Apr 11, 2017
09f83c8
Add two tests for potential caching problems
mstimberg Apr 25, 2017
3158274
Revert caching of `CodeGenerator.translate`
mstimberg Apr 26, 2017
04b0310
Fix a minor display issue for equations
mstimberg May 23, 2017
6d47a46
Fix a regression in `sympy_to_str`: function names where no longer tr…
mstimberg May 23, 2017
280f8d7
Merge branch 'master' into stateupdate_caching
mstimberg May 23, 2017
ca0e8a1
Merge branch 'master' into stateupdate_caching
mstimberg Jun 12, 2017
a6d74b9
Move the caching code into a `cached` decorator
mstimberg Aug 17, 2017
a56484f
Introduce a general `_state_tuple` function for all `Variable` objects
mstimberg Aug 18, 2017
407a0f8
Implement a general `_state_tuple` property for `SingleEquation` as well
mstimberg Aug 18, 2017
b070223
Remove unused function `replace_constants`
mstimberg Aug 18, 2017
598a74d
Add two small test cases for parsing/str-to-sympy conversion
mstimberg Aug 18, 2017
4298e47
Merge branch 'master' into stateupdate_caching
mstimberg Aug 18, 2017
f921573
Temporarily force use of Sphinx < 1.6.3
mstimberg Aug 18, 2017
404779d
Move `_state_tuple` code into `CacheKey` mixin class
mstimberg Aug 18, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Expand Up @@ -122,9 +122,9 @@ install:
conda install --yes --quiet anaconda-client conda-build jinja2 pip setuptools;
fi
- if [[ $MINIMAL_VERSIONS == 'yes' ]]; then
travis_retry conda create -n travis_conda --yes --quiet pip python=$PYTHON numpy==1.9 scipy==0.14 libgfortran==1 nose sphinx ipython sympy==0.7.6 jinja2==2.7 pyparsing setuptools coverage;
travis_retry conda create -n travis_conda --yes --quiet pip python=$PYTHON numpy==1.9 scipy==0.14 libgfortran==1 nose "sphinx<1.6.3" ipython sympy==0.7.6 jinja2==2.7 pyparsing setuptools coverage;
else
travis_retry conda create -n travis_conda --yes --quiet pip python=$PYTHON numpy nose sphinx ipython "sympy!=1.1.0" pyparsing jinja2 setuptools coverage;
travis_retry conda create -n travis_conda --yes --quiet pip python=$PYTHON numpy nose "sphinx<1.6.3" ipython "sympy!=1.1.0" pyparsing jinja2 setuptools coverage;
fi
- source activate travis_conda
# On Python 2: Install an older version of scipy that still has weave
Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Expand Up @@ -101,7 +101,7 @@ install:
- 'python -c "import struct; print(struct.calcsize(''P'') * 8)"'

# Install the build dependencies of the project via conda
- 'appveyor-retry conda install --yes --quiet numpy nose sphinx "sympy!=1.1.0" pyparsing jinja2 ipython setuptools cython'
- 'appveyor-retry conda install --yes --quiet numpy nose "sphinx<1.6.3" "sympy!=1.1.0" pyparsing jinja2 ipython setuptools cython'
# Install an older version of scipy (which still has weave) on Python 2
- 'if "%PYTHON_VERSION:~0,1%" == "2" appveyor-retry conda install --yes --quiet -c brian-team weave scipy'
- 'if "%PYTHON_VERSION:~0,1%" == "3" appveyor-retry conda install --yes --quiet scipy'
Expand Down
35 changes: 14 additions & 21 deletions brian2/core/variables.py
Expand Up @@ -88,7 +88,6 @@ def variables_by_owner(variables, owner):
return dict([(varname, var) for varname, var in variables.iteritems()
if getattr(var.owner, 'name', None) is owner_name])


class Variable(object):
'''
An object providing information about model variables (including implicit
Expand Down Expand Up @@ -165,6 +164,16 @@ def __init__(self, name, dimensions=DIMENSIONLESS, owner=None, dtype=None,
#: Whether the variable is an array
self.array = array

#: Mark the list of attributes that should not be considered for caching
#: of state update code, etc.
self._cache_irrelevant_attributes = ['owner']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a class attribute maybe? Not sure if it's worth doing but maybe slightly more logical.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed! It would even simplify the code a bit, since it wouldn't appear in __dict__ itself!


@property
def _state_tuple(self):
return tuple(value for key, value in self.__dict__.iteritems()
if key not in (self._cache_irrelevant_attributes +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to sort the items in a dict or is the order deterministic?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't matter here since the order is arbitrary but deterministic, but I'm not 100% sure there are no corner cases. So, using sorted sounds reasonable.

['_cache_irrelevant_attributes']))

@property
def is_boolean(self):
return np.issubdtype(np.bool, self.dtype)
Expand Down Expand Up @@ -274,9 +283,7 @@ def __repr__(self):
constant=repr(self.constant),
read_only=repr(self.read_only))

_state_tuple = property(lambda self: (self.dim, self.dtype, self.scalar,
self.constant, self.read_only,
self.dynamic, self.name))


# ------------------------------------------------------------------------------
# Concrete classes derived from `Variable` -- these are the only ones ever
Expand Down Expand Up @@ -341,11 +348,6 @@ def __init__(self, name, value, dimensions=DIMENSIONLESS, owner=None):
def get_value(self):
return self.value

# In contrast to other `Variable` objects, `Constant` objects are recreated
# for every run, so we cannot simply use their identity to compare them.
_state_tuple = property(lambda self: super(Constant, self)._state_tuple +
(self.value, ))


class AuxiliaryVariable(Variable):
'''
Expand Down Expand Up @@ -473,8 +475,6 @@ def get_addressable_value_with_unit(self, name, group):
return VariableView(name=name, variable=self, group=group,
dimensions=self.dim)

_state_tuple = property(lambda self: super(ArrayVariable, self)._state_tuple +
(self.size, self.unique, id(self.device)))

class DynamicArrayVariable(ArrayVariable):
'''
Expand Down Expand Up @@ -559,6 +559,9 @@ def __init__(self, name, owner, size, device, dimensions=DIMENSIONLESS,
dynamic=True,
read_only=read_only,
unique=unique)
# The size of a dynamic variable can of course change and changes in
# size should not invalidate the cache
self._cache_irrelevant_attributes += ['size']

@property
def dimensions(self):
Expand All @@ -584,13 +587,6 @@ def resize(self, new_size):

self.size = new_size

# Note that we inherit the state tuple from Variable, *not* from ArrayVariable.
# Otherwise the size would be part of the tuple, but of course the size of
# a dynamic variable can change.
_state_tuple = property(lambda self: super(ArrayVariable, self)._state_tuple +
(self.unique, id(self.device), self.ndim,
self.resize_along_first, self.needs_reference_update))


class Subexpression(Variable):
'''
Expand Down Expand Up @@ -670,9 +666,6 @@ def __repr__(self):
expr=repr(self.expr),
owner=self.owner.name)

_state_tuple = property(lambda self: super(Subexpression, self)._state_tuple +
(self.expr, id(self.device)))

# ------------------------------------------------------------------------------
# Classes providing views on variables and storing variables information
# ------------------------------------------------------------------------------
Expand Down
20 changes: 12 additions & 8 deletions brian2/equations/equations.py
Expand Up @@ -426,6 +426,18 @@ def __init__(self, type, varname, dimensions, var_type=FLOAT, expr=None,
# will be set later in the sort_subexpressions method of Equations
self.update_order = -1

#: Mark the list of attributes that should not be considered for caching
#: of state update code, etc.
self._cache_irrelevant_attributes = ['update_order']

@property
def _state_tuple(self):
'''A tuple representing the full state of this object, used for
hashing and equality testing.'''
return tuple(value for key, value in self.__dict__.iteritems()
if key not in (self._cache_irrelevant_attributes +
['_cache_irrelevant_attributes']))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could potentially be refactored since it's the same code as before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered it, but given that the function is almost trivial, I'm not sure it's worth introducing a common super-class for SingleEquation and Variable given that the classes don't have much in common... Hmm, maybe something like CacheKey could make sense, I'll have a look.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was thinking a sort of mixin class.


unit = property(lambda self: get_unit(self.dim),
doc='The `Unit` of this equation.')

Expand All @@ -437,14 +449,6 @@ def __init__(self, type, varname, dimensions, var_type=FLOAT, expr=None,
if variable =='xi' or variable.startswith('xi_')]),
doc='Stochastic variables in the RHS of this equation')


_state_tuple = property(lambda self: (self.type, self.varname,
self.dim, self.var_type,
self.expr, tuple(self.flags)),
doc='A tuple representing the full state of this '
'object, used for hashing and equality '
'testing.')

def __eq__(self, other):
if not isinstance(other, SingleEquation):
return NotImplemented
Expand Down
34 changes: 0 additions & 34 deletions brian2/parsing/sympytools.py
Expand Up @@ -165,40 +165,6 @@ def sympy_to_str(sympy_expr):
return expr


def replace_constants(sympy_expr, variables=None):
'''
Replace constant values in a sympy expression with their numerical value.

Parameters
----------
sympy_expr : `sympy.Expr`
The expression
variables : dict-like, optional
Dictionary of `Variable` objects

Returns
-------
new_expr : `sympy.Expr`
Expressions with all constants replaced
'''
if variables is None:
return sympy_expr

symbols = set([symbol for symbol in sympy_expr.atoms()
if isinstance(symbol, sympy.Symbol)])
for symbol in symbols:
symbol_str = str(symbol)
if symbol_str in variables:
var = variables[symbol_str]
if (getattr(var, 'scalar', False) and
getattr(var, 'constant', False)):
# TODO: We should handle variables of other data types better
float_val = var.get_value()
sympy_expr = sympy_expr.xreplace({symbol: sympy.Float(float_val)})

return sympy_expr


def expression_complexity(expr, complexity=None):
'''
Returns the complexity of an expression (either string or sympy)
Expand Down
6 changes: 4 additions & 2 deletions brian2/tests/test_parsing.py
Expand Up @@ -436,7 +436,8 @@ def test_sympytools():
'c * userfun(t + x)', # non-sympy function
'abs(x) + ceil(y)', # functions with a different name in sympy
'inf', # constant with a different name in sympy
]
'not(b)' # boolean expression
]

for expr in expressions:
expr2 = sympy_to_str(str_to_sympy(expr))
Expand All @@ -447,7 +448,8 @@ def test_error_messages():
nr = NodeRenderer()
expr_expected = [('3^2', '^', '**'),
('int(not_refractory | (v > 30))', '|', 'or'),
('int((v > 30) & (w < 20))', '&', 'and')]
('int((v > 30) & (w < 20))', '&', 'and'),
('x +* 3', '', '')]
for expr, expected_1, expected_2 in expr_expected:
try:
nr.render_expr(expr)
Expand Down
2 changes: 1 addition & 1 deletion brian2/utils/caching.py
Expand Up @@ -82,7 +82,7 @@ def _hashable(obj):

if isinstance(obj, set):
return frozenset(_hashable(el) for el in obj)
elif isinstance(obj, list):
elif isinstance(obj, collections.Sequence):
return tuple(_hashable(el) for el in obj)
elif isinstance(obj, collections.Mapping):
return frozenset((_hashable(key), _hashable(value))
Expand Down
2 changes: 1 addition & 1 deletion rtd-requirements.txt
Expand Up @@ -3,5 +3,5 @@ numpy>=1.4.1
pyparsing==1.5.7
sympy>=0.7.6
sphinxcontrib-issuetracker
sphinx>=1.5
sphinx>=1.5,<1.6.3
ipython