-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Cache parser * Update context docs * Update API docs * Mention data objects in specifying shapes docs * Add docs on performance * Bump version to 0.3.0
- Loading branch information
1 parent
490259b
commit 7c983b3
Showing
11 changed files
with
333 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
Performance | ||
=========== | ||
|
||
|
||
.. testsetup:: | ||
|
||
import numpy as np | ||
import numpy.typing as npt | ||
from numpy.random import randn | ||
from eincheck import ( | ||
check_func, check_data, check_shapes, | ||
disable_checks, enable_checks, | ||
parser_cache_clear, parser_cache_info, parser_resize_cache, | ||
) | ||
from typing import List | ||
|
||
Adding eincheck to a project introduces extra computations, which will have some latency impact. | ||
This impact can be significantly mitigated by following best practices. | ||
|
||
There are two major parts to performing shape checks with eincheck: parsing the user input into internal data structures and checking the tensor shapes against those data structures. | ||
In most cases, parsing will take significantly more time than actually doing the checks. | ||
Writing performant code with eincheck thus requires us to minimize the amount of parsing necessary. | ||
|
||
Doing Less Parsing | ||
------------------ | ||
|
||
There are several ways to reduce the amount of parsing. | ||
|
||
Use Decorators | ||
^^^^^^^^^^^^^^ | ||
|
||
The decorators ``check_data`` and ``check_func`` will parse the inputs once and then reuse them each time the data object/function is called. | ||
These decorators should be used whenever possible. | ||
|
||
Cached Parsing | ||
^^^^^^^^^^^^^^ | ||
There are cases where the abovementioned decorators cannot be used. | ||
``check_shapes`` uses an ``lru_cache`` to cache parsing, initialized with a default size of 128. | ||
To achieve good cache utilization, prefer to use constant shape specs. | ||
For example: | ||
|
||
.. doctest:: | ||
|
||
>>> parser_cache_clear() | ||
>>> parser_cache_info() | ||
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0) | ||
>>> def bad(x: npt.NDArray[np.float64], inds: List[int]) -> npt.NDArray[np.float64]: | ||
... y = x[..., inds] | ||
... # Bad! The shape spec for y will change for different length inds. | ||
... check_shapes((x, "*i _"), (y, f"*i {len(inds)}")) | ||
... return y | ||
>>> _ = bad(randn(5, 10), [1]) | ||
>>> _ = bad(randn(5, 10), [4, 2]) | ||
>>> _ = bad(randn(5, 10), [0, 1, 2]) | ||
>>> _ = bad(randn(5, 10), [7, 7, 7, 7, 7]) | ||
>>> _ = bad(randn(5, 10), [3, 1, 4, 1, 5]) | ||
>>> parser_cache_info() | ||
CacheInfo(hits=5, misses=5, maxsize=128, currsize=5) | ||
>>> | ||
>>> parser_cache_clear() | ||
>>> def good(x: npt.NDArray[np.float64], inds: List[int]) -> npt.NDArray[np.float64]: | ||
... y = x[..., inds] | ||
... # Good! The shape specs are constant. | ||
... check_shapes((x, "*i _"), (y, "*i n"), n=len(inds)) | ||
... return y | ||
>>> _ = good(randn(5, 10), [1]) | ||
>>> _ = good(randn(5, 10), [4, 2]) | ||
>>> _ = good(randn(5, 10), [0, 1, 2]) | ||
>>> _ = good(randn(5, 10), [7, 7, 7, 7, 7]) | ||
>>> _ = good(randn(5, 10), [3, 1, 4, 1, 5]) | ||
>>> parser_cache_info() | ||
CacheInfo(hits=8, misses=2, maxsize=128, currsize=2) | ||
|
||
The functions ``parser_cache_info``, ``parser_cache_clear``, and ``parser_resize_cache`` can be used to monitor and adjust the caching behavior. | ||
|
||
Skip Parsing | ||
^^^^^^^^^^^^ | ||
|
||
If caching is not a viable option (e.g. due to memory constraints), another option to improve performance is to pass lists instead of strings as the shape specs to ``check_shapes``. | ||
The ``ShapeArg`` type used as input to ``check_shapes`` includes ``Sequence[Union[DimSpec, str, int, None]]``. | ||
If a sequence is provided, eincheck will skip the parser and build the internal data structures directly from this list. | ||
As such, advanced parsing features are not supported with this method. | ||
The strings in the sequence must be single variable names, with no parentheses or binary operators. | ||
|
||
.. doctest:: | ||
|
||
>>> def foo(x: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: | ||
... check_shapes((x, [None, "i", "i"])) | ||
... return np.diagonal(x, axis1=1, axis2=2) | ||
>>> foo(randn(3, 4, 4)).shape | ||
(3, 4) | ||
>>> def bad(x: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: | ||
... # Bad! Can't use binary operator + with list ShapeArg. | ||
... check_shapes((x, ["i", "i+1"])) | ||
... return x | ||
>>> bad(randn(3, 4)).shape | ||
Traceback (most recent call last): | ||
... | ||
ValueError: Variable name should be made of only ascii letters, got i+1 | ||
|
||
Disabling Checks | ||
---------------- | ||
|
||
The most powerful tool to make code using eincheck run faster is to disable eincheck altogether. | ||
For example, eincheck can be used while initially developing code and then disabled in optimized production environments. | ||
The ``disable_checks`` and ``enable_checks`` context managers can be used to disable and re-enable eincheck within certain scopes. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from functools import _CacheInfo, lru_cache | ||
from typing import Callable, Generic, Optional, TypeVar | ||
|
||
from typing_extensions import ParamSpec | ||
|
||
_T = TypeVar("_T") | ||
_P = ParamSpec("_P") | ||
|
||
|
||
class ResizeableLruCache(Generic[_P, _T]): | ||
def __init__(self, func: Callable[_P, _T], maxsize: Optional[int] = 128): | ||
self._func = func | ||
self._cached_func = lru_cache(maxsize=maxsize)(func) | ||
|
||
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _T: | ||
return self._cached_func(*args, **kwargs) # type: ignore[arg-type] | ||
|
||
def reset_maxsize(self, maxsize: Optional[int]) -> None: | ||
self._cached_func = lru_cache(maxsize=maxsize)(self._func) | ||
|
||
def cache_info(self) -> _CacheInfo: | ||
return self._cached_func.cache_info() | ||
|
||
def cache_clear(self) -> None: | ||
self._cached_func.cache_clear() | ||
|
||
|
||
def resizeable_lru_cache( | ||
maxsize: Optional[int] = 128, | ||
) -> Callable[[Callable[_P, _T]], ResizeableLruCache[_P, _T]]: | ||
"""Resizable version of functools.lru_cache.""" | ||
return lambda f: ResizeableLruCache(f, maxsize=maxsize) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from eincheck.parser.grammar import parse_shape_spec | ||
|
||
parser_cache_info = parse_shape_spec.cache_info | ||
parser_cache_clear = parse_shape_spec.cache_clear | ||
parser_resize_cache = parse_shape_spec.reset_maxsize |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.