stringjax-tools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stringjax_tools/__init__.py +68 -0
- stringjax_tools/__main__.py +25 -0
- stringjax_tools/_utils.py +201 -0
- stringjax_tools/auto_vectorise.py +581 -0
- stringjax_tools/cache.py +86 -0
- stringjax_tools/jit.py +82 -0
- stringjax_tools/py.typed +1 -0
- stringjax_tools/pytrees.py +331 -0
- stringjax_tools/vmap.py +114 -0
- stringjax_tools-0.1.0.dist-info/METADATA +215 -0
- stringjax_tools-0.1.0.dist-info/RECORD +14 -0
- stringjax_tools-0.1.0.dist-info/WHEEL +5 -0
- stringjax_tools-0.1.0.dist-info/licenses/LICENSE +674 -0
- stringjax_tools-0.1.0.dist-info/top_level.txt +1 -0
stringjax_tools/jit.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright 2022-2026 Andreas Schachner
|
|
2
|
+
#
|
|
3
|
+
# This file is part of StringJAX Tools.
|
|
4
|
+
#
|
|
5
|
+
# StringJAX Tools is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# StringJAX Tools is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with StringJAX Tools. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
"""Small JIT helpers for functions with Python-side static arguments."""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from functools import wraps
|
|
23
|
+
from typing import Any, Callable
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"is_static",
|
|
30
|
+
"jit_with_dynamic_static_args",
|
|
31
|
+
"jit_with_static_args",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def jit_with_static_args(
|
|
36
|
+
func: Callable[..., Any],
|
|
37
|
+
static_argnums: tuple[int, ...] = (),
|
|
38
|
+
) -> Callable[..., Any]:
|
|
39
|
+
r"""
|
|
40
|
+
Wrap ``func`` with ``jax.jit`` and explicit positional static arguments.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
func: Function to JIT-compile.
|
|
44
|
+
static_argnums: Positional argument indices to mark static.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
JIT-compiled wrapper around ``func``.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@wraps(func)
|
|
51
|
+
def call_func(*args: Any, **kwargs: Any) -> Any:
|
|
52
|
+
return func(*args, **kwargs)
|
|
53
|
+
|
|
54
|
+
return jax.jit(call_func, static_argnums=static_argnums)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_static(arg: Any) -> bool:
|
|
58
|
+
r"""
|
|
59
|
+
Heuristic test for whether ``arg`` should be treated as JAX-static.
|
|
60
|
+
|
|
61
|
+
Python scalars, strings, booleans and ordinary configuration objects are
|
|
62
|
+
treated as static. JAX arrays and NumPy arrays are treated as dynamic.
|
|
63
|
+
"""
|
|
64
|
+
return not isinstance(arg, (jax.Array, np.ndarray))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def jit_with_dynamic_static_args(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
68
|
+
r"""
|
|
69
|
+
JIT ``func`` while deciding static positional arguments at call time.
|
|
70
|
+
|
|
71
|
+
This is convenient for diagnostics and prototypes, but production code
|
|
72
|
+
should prefer :func:`jit_with_static_args` with explicit ``static_argnums``
|
|
73
|
+
because changing static positions can cause repeated compilation.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@wraps(func)
|
|
77
|
+
def wrapped_func(*args: Any, **kwargs: Any) -> Any:
|
|
78
|
+
static_argnums = tuple(i for i, arg in enumerate(args) if is_static(arg))
|
|
79
|
+
jit_func = jax.jit(func, static_argnums=static_argnums)
|
|
80
|
+
return jit_func(*args, **kwargs)
|
|
81
|
+
|
|
82
|
+
return wrapped_func
|
stringjax_tools/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Copyright 2022-2026 Andreas Schachner
|
|
2
|
+
#
|
|
3
|
+
# This file is part of StringJAX Tools.
|
|
4
|
+
#
|
|
5
|
+
# StringJAX Tools is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# StringJAX Tools is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with StringJAX Tools. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
"""Configurable pytree registration helpers for stateful model classes.
|
|
19
|
+
|
|
20
|
+
JAX reconstructs custom pytree nodes with the registered unflatten function,
|
|
21
|
+
not by calling ``__init__``. Stateful classes therefore need a deliberate state
|
|
22
|
+
policy: semantic configuration should be preserved as static auxiliary data,
|
|
23
|
+
numerical leaves should be traced, and ignored attributes should be limited to
|
|
24
|
+
recomputable eager caches or scratch state.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from collections.abc import Iterable, Mapping
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from types import MappingProxyType
|
|
32
|
+
from typing import Any, Callable
|
|
33
|
+
|
|
34
|
+
import jax
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"PytreePolicy",
|
|
39
|
+
"flatten_func",
|
|
40
|
+
"make_pytree_flatteners",
|
|
41
|
+
"unflatten_func_class",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
AuxData = tuple[tuple[str, ...], tuple[tuple[str, Any], ...]]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _as_tuple(values: Iterable[Any] | None) -> tuple[Any, ...]:
|
|
48
|
+
r"""Normalise optional iterables to tuples."""
|
|
49
|
+
if values is None:
|
|
50
|
+
return ()
|
|
51
|
+
if isinstance(values, str):
|
|
52
|
+
return (values,)
|
|
53
|
+
return tuple(values)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _as_key_tuple(kind: str, values: Iterable[str] | None) -> tuple[str, ...]:
|
|
57
|
+
r"""Normalise and validate attribute-name iterables."""
|
|
58
|
+
keys = _as_tuple(values)
|
|
59
|
+
invalid = [key for key in keys if not isinstance(key, str)]
|
|
60
|
+
if invalid:
|
|
61
|
+
raise ValueError(f"{kind} must contain only string attribute names, got {invalid!r}.")
|
|
62
|
+
return keys
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _validate_disjoint_keys(static_keys: Iterable[str], ignore_keys: Iterable[str]) -> None:
|
|
66
|
+
r"""Reject ambiguous pytree policies where static state would be ignored."""
|
|
67
|
+
overlap = sorted(set(static_keys).intersection(ignore_keys))
|
|
68
|
+
if overlap:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"Pytree attributes cannot be both static and ignored: "
|
|
71
|
+
f"{overlap}. Keep semantic state static and ignore only caches."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _validate_static_value(key: str, value: Any) -> None:
|
|
76
|
+
r"""Raise if ``value`` is unsuitable for JAX pytree auxiliary data."""
|
|
77
|
+
try:
|
|
78
|
+
hash(value)
|
|
79
|
+
except TypeError as exc:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Static pytree attribute {key!r} is not hashable. "
|
|
82
|
+
"Use validate_static=False only if you know JAX can safely carry "
|
|
83
|
+
"this value as auxiliary data, or remove it from static_keys."
|
|
84
|
+
) from exc
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
equality = value == value
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Static pytree attribute {key!r} has unstable equality semantics."
|
|
91
|
+
) from exc
|
|
92
|
+
|
|
93
|
+
if not isinstance(equality, (bool, np.bool_)):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Static pytree attribute {key!r} does not compare to a scalar bool. "
|
|
96
|
+
"This usually means an array-like object was marked static."
|
|
97
|
+
)
|
|
98
|
+
if not bool(equality):
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Static pytree attribute {key!r} is not self-equal. "
|
|
101
|
+
"This usually means a NaN-like value was marked static."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def flatten_func(
|
|
106
|
+
obj: Any,
|
|
107
|
+
*,
|
|
108
|
+
static_keys: Iterable[str] = (),
|
|
109
|
+
ignore_keys: Iterable[str] = (),
|
|
110
|
+
static_types: tuple[type, ...] = (str, bool),
|
|
111
|
+
validate_static: bool = True,
|
|
112
|
+
) -> tuple[tuple[Any, ...], AuxData]:
|
|
113
|
+
r"""
|
|
114
|
+
Flatten ``obj`` for the JAX pytree protocol.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
obj: Instance to flatten.
|
|
118
|
+
static_keys: Attribute names stored as static auxiliary data.
|
|
119
|
+
ignore_keys: Attribute names excluded from the pytree entirely. These
|
|
120
|
+
attributes are absent after reconstruction unless the consuming
|
|
121
|
+
package restores them in a custom unflatten wrapper. Do not put
|
|
122
|
+
semantic configuration or user input here.
|
|
123
|
+
static_types: Python value types that should always be static.
|
|
124
|
+
validate_static: If ``True``, reject unhashable or array-like static
|
|
125
|
+
values before they enter JAX auxiliary data.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
``(children, aux_data)`` in the form required by
|
|
129
|
+
``jax.tree_util.register_pytree_node``. ``aux_data`` is structured as
|
|
130
|
+
``(child_keys, static_items)``.
|
|
131
|
+
"""
|
|
132
|
+
static_key_tuple = _as_key_tuple("static_keys", static_keys)
|
|
133
|
+
ignore_key_tuple = _as_key_tuple("ignore_keys", ignore_keys)
|
|
134
|
+
_validate_disjoint_keys(static_key_tuple, ignore_key_tuple)
|
|
135
|
+
|
|
136
|
+
static_key_set = set(static_key_tuple)
|
|
137
|
+
ignore_key_set = set(ignore_key_tuple)
|
|
138
|
+
|
|
139
|
+
children: list[Any] = []
|
|
140
|
+
child_keys: list[str] = []
|
|
141
|
+
static_items: list[tuple[str, Any]] = []
|
|
142
|
+
|
|
143
|
+
for key, value in vars(obj).items():
|
|
144
|
+
if key in ignore_key_set:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if key in static_key_set or isinstance(value, static_types):
|
|
148
|
+
if validate_static:
|
|
149
|
+
_validate_static_value(key, value)
|
|
150
|
+
static_items.append((key, value))
|
|
151
|
+
else:
|
|
152
|
+
children.append(value)
|
|
153
|
+
child_keys.append(key)
|
|
154
|
+
|
|
155
|
+
return tuple(children), (tuple(child_keys), tuple(static_items))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _materialise_default(default: Any) -> Any:
|
|
159
|
+
r"""Return an ignored-attribute default, calling factories when supplied."""
|
|
160
|
+
return default() if callable(default) else default
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def unflatten_func_class(
|
|
164
|
+
aux_data: AuxData,
|
|
165
|
+
children: tuple[Any, ...],
|
|
166
|
+
myclass: type,
|
|
167
|
+
*,
|
|
168
|
+
ignore_defaults: Mapping[str, Any] | None = None,
|
|
169
|
+
) -> Any:
|
|
170
|
+
r"""
|
|
171
|
+
Reconstruct ``myclass`` from pytree auxiliary data and children.
|
|
172
|
+
|
|
173
|
+
``__init__`` is intentionally bypassed. This mirrors the standard pattern
|
|
174
|
+
for JAX pytree registration of stateful objects whose constructors may
|
|
175
|
+
perform validation, IO, or other eager side effects.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
aux_data: Auxiliary data returned by :func:`flatten_func`.
|
|
179
|
+
children: Dynamic children returned by :func:`flatten_func`.
|
|
180
|
+
myclass: Class to reconstruct.
|
|
181
|
+
ignore_defaults: Optional defaults for ignored attributes. Callable
|
|
182
|
+
defaults such as ``dict`` are called each time, so reconstructed
|
|
183
|
+
objects do not share mutable containers.
|
|
184
|
+
"""
|
|
185
|
+
child_keys, static_items = aux_data
|
|
186
|
+
if len(child_keys) != len(children):
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"Pytree aux_data and children are inconsistent: "
|
|
189
|
+
f"{len(child_keys)} child key(s) for {len(children)} child value(s)."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
obj = object.__new__(myclass)
|
|
193
|
+
for key, value in zip(child_keys, children, strict=True):
|
|
194
|
+
object.__setattr__(obj, key, value)
|
|
195
|
+
for key, value in static_items:
|
|
196
|
+
object.__setattr__(obj, key, value)
|
|
197
|
+
for key, default in (ignore_defaults or {}).items():
|
|
198
|
+
object.__setattr__(obj, key, _materialise_default(default))
|
|
199
|
+
return obj
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class PytreePolicy:
|
|
204
|
+
r"""
|
|
205
|
+
Shared pytree flattening policy for one package or model family.
|
|
206
|
+
|
|
207
|
+
A StringJAX package should define its domain-specific ``static_keys`` and
|
|
208
|
+
ignored-cache policy locally, then reuse one policy to register all classes
|
|
209
|
+
that share the same state conventions.
|
|
210
|
+
|
|
211
|
+
Warning:
|
|
212
|
+
Ignored attributes are suitable for recomputable caches, scratch arrays
|
|
213
|
+
and eager-only helpers, but not for semantic state such as user-supplied
|
|
214
|
+
bounds, physical parameters or configuration values. Put such state in
|
|
215
|
+
``static_keys`` when it is hashable and immutable, or keep it as a
|
|
216
|
+
traced child when it is array-like. If an ignored cache may be read
|
|
217
|
+
after reconstruction, declare an ``ignore_defaults`` entry so unflatten
|
|
218
|
+
restores a safe value such as ``None`` or a fresh ``dict``.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
static_keys: Iterable[str] = ()
|
|
222
|
+
ignore_keys: Iterable[str] = ()
|
|
223
|
+
ignore_defaults: Mapping[str, Any] | None = None
|
|
224
|
+
static_types: tuple[type, ...] = (str, bool)
|
|
225
|
+
validate_static: bool = True
|
|
226
|
+
|
|
227
|
+
def __post_init__(self) -> None:
|
|
228
|
+
r"""Freeze iterable configuration so the policy is stable."""
|
|
229
|
+
ignore_defaults = dict(self.ignore_defaults or {})
|
|
230
|
+
invalid_defaults = [
|
|
231
|
+
key for key in ignore_defaults
|
|
232
|
+
if not isinstance(key, str)
|
|
233
|
+
]
|
|
234
|
+
if invalid_defaults:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"ignore_defaults must contain only string attribute names, "
|
|
237
|
+
f"got {invalid_defaults!r}."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
static_keys = _as_key_tuple("static_keys", self.static_keys)
|
|
241
|
+
ignore_keys = tuple(
|
|
242
|
+
dict.fromkeys(
|
|
243
|
+
_as_key_tuple("ignore_keys", self.ignore_keys)
|
|
244
|
+
+ tuple(ignore_defaults)
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
_validate_disjoint_keys(static_keys, ignore_keys)
|
|
248
|
+
|
|
249
|
+
object.__setattr__(self, "static_keys", static_keys)
|
|
250
|
+
object.__setattr__(
|
|
251
|
+
self,
|
|
252
|
+
"ignore_keys",
|
|
253
|
+
ignore_keys,
|
|
254
|
+
)
|
|
255
|
+
object.__setattr__(
|
|
256
|
+
self,
|
|
257
|
+
"ignore_defaults",
|
|
258
|
+
MappingProxyType(ignore_defaults),
|
|
259
|
+
)
|
|
260
|
+
object.__setattr__(self, "static_types", tuple(self.static_types))
|
|
261
|
+
|
|
262
|
+
def flatten(self, obj: Any) -> tuple[tuple[Any, ...], AuxData]:
|
|
263
|
+
r"""Flatten ``obj`` using this policy."""
|
|
264
|
+
return flatten_func(
|
|
265
|
+
obj,
|
|
266
|
+
static_keys=self.static_keys,
|
|
267
|
+
ignore_keys=self.ignore_keys,
|
|
268
|
+
static_types=self.static_types,
|
|
269
|
+
validate_static=self.validate_static,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def unflatten(
|
|
273
|
+
self,
|
|
274
|
+
aux_data: AuxData,
|
|
275
|
+
children: tuple[Any, ...],
|
|
276
|
+
myclass: type,
|
|
277
|
+
) -> Any:
|
|
278
|
+
r"""Reconstruct ``myclass`` from pytree data."""
|
|
279
|
+
return unflatten_func_class(
|
|
280
|
+
aux_data,
|
|
281
|
+
children,
|
|
282
|
+
myclass,
|
|
283
|
+
ignore_defaults=self.ignore_defaults,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def make_flatteners(
|
|
287
|
+
self,
|
|
288
|
+
myclass: type,
|
|
289
|
+
) -> tuple[Callable[[Any], tuple[tuple[Any, ...], AuxData]], Callable[[AuxData, tuple[Any, ...]], Any]]:
|
|
290
|
+
r"""Return class-specific ``(flatten, unflatten)`` callables."""
|
|
291
|
+
|
|
292
|
+
def unflatten(aux_data: AuxData, children: tuple[Any, ...]) -> Any:
|
|
293
|
+
return self.unflatten(aux_data, children, myclass)
|
|
294
|
+
|
|
295
|
+
return self.flatten, unflatten
|
|
296
|
+
|
|
297
|
+
def register(self, myclass: type) -> type:
|
|
298
|
+
r"""
|
|
299
|
+
Register ``myclass`` as a JAX pytree node and return the class.
|
|
300
|
+
|
|
301
|
+
Returning the class lets this method be used either directly or as a
|
|
302
|
+
decorator.
|
|
303
|
+
"""
|
|
304
|
+
flatten, unflatten = self.make_flatteners(myclass)
|
|
305
|
+
jax.tree_util.register_pytree_node(myclass, flatten, unflatten)
|
|
306
|
+
return myclass
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def make_pytree_flatteners(
|
|
310
|
+
myclass: type,
|
|
311
|
+
*,
|
|
312
|
+
static_keys: Iterable[str] = (),
|
|
313
|
+
ignore_keys: Iterable[str] = (),
|
|
314
|
+
ignore_defaults: Mapping[str, Any] | None = None,
|
|
315
|
+
static_types: tuple[type, ...] = (str, bool),
|
|
316
|
+
validate_static: bool = True,
|
|
317
|
+
) -> tuple[Callable[[Any], tuple[tuple[Any, ...], AuxData]], Callable[[AuxData, tuple[Any, ...]], Any]]:
|
|
318
|
+
r"""
|
|
319
|
+
Build class-specific pytree flatten/unflatten callables.
|
|
320
|
+
|
|
321
|
+
This convenience wrapper is useful when a project wants explicit
|
|
322
|
+
registration calls without first naming a shared :class:`PytreePolicy`.
|
|
323
|
+
"""
|
|
324
|
+
policy = PytreePolicy(
|
|
325
|
+
static_keys=static_keys,
|
|
326
|
+
ignore_keys=ignore_keys,
|
|
327
|
+
ignore_defaults=ignore_defaults,
|
|
328
|
+
static_types=static_types,
|
|
329
|
+
validate_static=validate_static,
|
|
330
|
+
)
|
|
331
|
+
return policy.make_flatteners(myclass)
|
stringjax_tools/vmap.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Copyright 2022-2026 Andreas Schachner
|
|
2
|
+
#
|
|
3
|
+
# This file is part of StringJAX Tools.
|
|
4
|
+
#
|
|
5
|
+
# StringJAX Tools is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# StringJAX Tools is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with StringJAX Tools. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
"""Manual cached ``vmap``/``jit`` factories."""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections import OrderedDict
|
|
23
|
+
from typing import Any, Callable
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
|
|
27
|
+
from ._utils import cache_key_value, lru_cache_get, lru_cache_set, snapshot_static_value
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"clear_vmap_caches",
|
|
31
|
+
"vmapping_func",
|
|
32
|
+
"vmapping_func_cached",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_VMAP_JIT_CACHE_MAXSIZE = 256
|
|
36
|
+
_VMAP_JIT_CACHE: OrderedDict[tuple[Any, ...], Callable[..., Any]] = OrderedDict()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def vmapping_func(
|
|
40
|
+
func: Callable[..., Any],
|
|
41
|
+
in_axes: int | tuple[Any, ...] | None = None,
|
|
42
|
+
**kwargs: Any,
|
|
43
|
+
) -> Callable[..., Any]:
|
|
44
|
+
r"""
|
|
45
|
+
Return a fresh ``jax.jit(jax.vmap(...))`` wrapper around ``func``.
|
|
46
|
+
|
|
47
|
+
``kwargs`` are bound inside the mapped closure. Each call constructs a new
|
|
48
|
+
wrapper object, so repeated use should normally prefer
|
|
49
|
+
:func:`vmapping_func_cached`.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def mapped_call(*args: Any) -> Any:
|
|
53
|
+
return func(*args, **kwargs)
|
|
54
|
+
|
|
55
|
+
return jax.jit(jax.vmap(mapped_call, in_axes=in_axes))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _build_vmap_jit(
|
|
59
|
+
func: Callable[..., Any],
|
|
60
|
+
in_axes: int | tuple[Any, ...] | None,
|
|
61
|
+
frozen_kwargs: tuple[tuple[str, Any], ...],
|
|
62
|
+
) -> Callable[..., Any]:
|
|
63
|
+
r"""
|
|
64
|
+
Build one ``jax.jit(jax.vmap(...))`` wrapper.
|
|
65
|
+
|
|
66
|
+
This function is intentionally small and internal: cache lookup and
|
|
67
|
+
cache-key normalisation happen in :func:`vmapping_func_cached`.
|
|
68
|
+
"""
|
|
69
|
+
kwargs = dict(frozen_kwargs)
|
|
70
|
+
|
|
71
|
+
def mapped_call(*args: Any) -> Any:
|
|
72
|
+
return func(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
return jax.jit(jax.vmap(mapped_call, in_axes=in_axes))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def vmapping_func_cached(
|
|
78
|
+
func: Callable[..., Any],
|
|
79
|
+
in_axes: int | tuple[Any, ...] | None = None,
|
|
80
|
+
**kwargs: Any,
|
|
81
|
+
) -> Callable[..., Any]:
|
|
82
|
+
r"""
|
|
83
|
+
Return a cached ``jax.jit(jax.vmap(...))`` wrapper around ``func``.
|
|
84
|
+
|
|
85
|
+
The cache key includes the callable, ``in_axes``, and a robust key for
|
|
86
|
+
bound keyword configuration. Keyword values may be unhashable; they are
|
|
87
|
+
used by the closure, while their cache-key representation is produced by
|
|
88
|
+
:func:`stringjax_tools._utils.cache_key_value`.
|
|
89
|
+
"""
|
|
90
|
+
kwargs_items = tuple(
|
|
91
|
+
(key, snapshot_static_value(value))
|
|
92
|
+
for key, value in sorted(kwargs.items(), key=lambda item: item[0])
|
|
93
|
+
)
|
|
94
|
+
cache_key = (
|
|
95
|
+
func,
|
|
96
|
+
cache_key_value(in_axes),
|
|
97
|
+
tuple((key, cache_key_value(value)) for key, value in kwargs_items),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
cached = lru_cache_get(_VMAP_JIT_CACHE, cache_key)
|
|
101
|
+
if cached is None:
|
|
102
|
+
cached = _build_vmap_jit(func, in_axes, kwargs_items)
|
|
103
|
+
lru_cache_set(
|
|
104
|
+
_VMAP_JIT_CACHE,
|
|
105
|
+
cache_key,
|
|
106
|
+
cached,
|
|
107
|
+
maxsize=_VMAP_JIT_CACHE_MAXSIZE,
|
|
108
|
+
)
|
|
109
|
+
return cached
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def clear_vmap_caches() -> None:
|
|
113
|
+
r"""Clear wrappers cached by :func:`vmapping_func_cached`."""
|
|
114
|
+
_VMAP_JIT_CACHE.clear()
|