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/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
@@ -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)
@@ -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()