Skip to content

Commit 3c7a018

Browse files
committed
✨ feat(nn): AbstractScanNNTrainer for scan-over-epoch NN training
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
1 parent 1f7733d commit 3c7a018

12 files changed

Lines changed: 3163 additions & 12 deletions

File tree

README.md

Lines changed: 437 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ classifiers = [
1919
"Topic :: Scientific/Engineering",
2020
"Typing :: Typed",
2121
]
22-
dependencies = ["equinox>=0.11.0", "jax>=0.4.20"]
22+
dependencies = ["jax>=0.4.20", "jaxtyping>=0.3.7", "optional_dependencies>=0.3"]
2323
description = "There's more to JAX."
2424
dynamic = ["version"]
2525
license.file = "LICENSE"
@@ -33,6 +33,10 @@ Changelog = "https://github.com/GalacticDynamics/jaxmore/releases"
3333
Discussions = "https://github.com/GalacticDynamics/jaxmore/discussions"
3434
Homepage = "https://github.com/GalacticDynamics/jaxmore"
3535

36+
[project.optional-dependencies]
37+
equinox = ["equinox>=0.11.0"]
38+
tqdm = ["jax-tqdm>=0.1.0"]
39+
3640

3741
[build-system]
3842
build-backend = "hatchling.build"
@@ -61,13 +65,15 @@ lint = ["mypy>=1.19.0", "pre-commit>=4.1.0", "pylint>=3.3.8"]
6165
nox = ["nox>=2024.10.9", "nox-uv>=0.6.3"]
6266
test = [
6367
"beartype>=0.19.0",
68+
"equinox>=0.11.0",
69+
"jax-tqdm>=0.1.0",
6470
"jaxtyping>=0.2.34",
65-
"optional_dependencies>=0.3",
71+
"optax>=0.2.0",
6672
"pytest>=8.4.2",
6773
"pytest-benchmark>=5.1.0",
6874
"pytest-cov>=6.2.1",
6975
"pytest-github-actions-annotate-failures>=0.3.0",
70-
"sybil[pytest]>=9.2.0",
76+
"sybil>=9.2.0",
7177
]
7278

7379

@@ -101,7 +107,10 @@ addopts = [
101107
"-p no:doctest", # using sybil
102108
"-ra",
103109
]
104-
filterwarnings = ["error"]
110+
filterwarnings = [
111+
"error",
112+
"ignore:Setting `jax_pmap_shmap_merge` is deprecated:DeprecationWarning",
113+
]
105114
log_cli_level = "INFO"
106115
markers = ["benchmark: benchmarks (deselect with '-m \"not benchmark\"')"]
107116
minversion = "8.3"
@@ -172,6 +181,10 @@ ignore-paths = [".*/_version.py"]
172181
messages_control.disable = [
173182
"design",
174183
"fixme",
184+
"import-error",
185+
"import-outside-toplevel",
186+
"invalid-enum-extension",
187+
"invalid-name",
175188
"line-too-long",
176189
"missing-function-docstring",
177190
"missing-module-docstring",

src/jaxmore/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Copyright (c) 2026 Nathaniel Starkman. All rights reserved."""
22

3-
__all__ = ("bounded_while_loop", "structured", "vmap")
3+
__all__ = ("bounded_while_loop", "nn", "structured", "vmap")
44

5+
6+
from . import nn
57
from ._src import bounded_while_loop, structured, vmap

src/jaxmore/_src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Jaxmore private implementation."""
22

3+
from .error import * # noqa: F403
34
from .structured import * # noqa: F403
45
from .vmap_ext import * # noqa: F403
56
from .while_loop import * # noqa: F403

src/jaxmore/_src/error.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""Copyright (c) 2026 Nathaniel Starkman. All rights reserved."""
2+
3+
__all__ = ("error_if",)
4+
5+
from typing import Any, Literal, TypeVar
6+
7+
import jax
8+
import jax.numpy as jnp
9+
10+
T = TypeVar("T")
11+
12+
13+
def error_if(
14+
value: T,
15+
condition: Any,
16+
msg: str,
17+
/,
18+
*,
19+
on_error: Literal["raise", "off"] = "raise",
20+
) -> T:
21+
"""Raise an error if a condition is true (JAX-compatible).
22+
23+
This function is compatible with JAX transformations (jit, vmap, grad) and
24+
raises an error at runtime if the condition evaluates to True. The raised
25+
exception is always `jax.errors.JaxRuntimeError`.
26+
27+
Uses `jax.debug.callback` internally to execute the error-raising logic
28+
outside of JAX's tracing system. This has important consequences:
29+
30+
- **Runtime execution**: The callback executes at runtime, not during
31+
tracing, so it can inspect actual runtime values (not tracer objects).
32+
- **CPU execution**: The callback always runs on CPU, even if the rest of
33+
the JAX computation is on GPU/TPU. This is a mild performance cost but
34+
necessary for error handling.
35+
- **Not dead code eliminated**: Despite using a callback, the error check is
36+
NOT eliminated by JAX's optimizations (unlike pure computations that may
37+
be DCE'd). The callback is considered a side effect and is preserved.
38+
- **With jit/vmap**: Works seamlessly inside `jax.jit` and `jax.vmap`
39+
contexts. When vmapped, the callback is called for each mapped instance.
40+
- **Exception wrapping**: Under `jax.jit`, exceptions raised from
41+
`jax.debug.callback` are typically wrapped by JAX as
42+
`jax.errors.JaxRuntimeError` (backend-dependent). Outside jit, the
43+
`JaxRuntimeError` is raised directly.
44+
45+
Parameters
46+
----------
47+
value
48+
The value to return if no error is raised.
49+
condition
50+
A boolean or JAX array (scalar or multi-element) indicating whether to
51+
raise an error. If an array, the error is raised if any element is True.
52+
msg
53+
The error message to raise if condition is True.
54+
on_error
55+
Controls error handling behavior:
56+
57+
- ``"raise"`` (default): Raise an error when condition is True.
58+
- ``"off"``:
59+
Disable error checking entirely. This is a complete no-op that skips
60+
even the callback, providing maximum performance when error checks
61+
are not needed.
62+
63+
Returns
64+
-------
65+
T
66+
The input value if condition is False, otherwise an error is raised.
67+
68+
Examples
69+
--------
70+
Basic usage (raises error if condition is true):
71+
72+
.. code-block:: python
73+
74+
import jax.numpy as jnp
75+
from jaxmore._src import error_if
76+
77+
x = jnp.array(5)
78+
result = error_if(x, x > 10, "x is too large")
79+
# result = Array(5, dtype=int32)
80+
81+
Disable error checking for performance:
82+
83+
.. code-block:: python
84+
85+
import jax.numpy as jnp
86+
from jaxmore._src import error_if
87+
88+
x = jnp.array(15)
89+
result = error_if(x, x > 10, "ignored", on_error="off")
90+
# No callback is executed; this is a complete no-op
91+
92+
Notes
93+
-----
94+
For most production code requiring flexible error handling,
95+
`equinox.error_if` is recommended. The `equinox.error_if` implementation is
96+
generally more feature-rich and better integrated with the broader
97+
JAX/Equinox ecosystem:
98+
99+
- **Multiple error handling modes**: Equinox supports ``"raise"``,
100+
``"breakpoint"`` (with configurable debugger frames), ``"nan"`` (replace
101+
values and continue), ``"warn"`` (emit warning), and ``"off"``.
102+
- **Better error messages**: Integration with `equinox.filter_jit` provides
103+
cleaner stack traces and more informative error reporting.
104+
- **Platform-specific handling**: Special logic for TPU runtime, which
105+
normally squelches errors.
106+
- **Automatic differentiation**: Custom JVP rules ensure correct behavior
107+
under `jax.grad` and other AD transforms.
108+
- **Dead code elimination awareness**: Uses `lax.cond` to conditionally
109+
branch, allowing JAX's compiler to optimize away unused checks (though
110+
this requires the return value to be used in the computation).
111+
112+
"""
113+
if on_error == "off":
114+
# Complete no-op: skip the callback entirely for maximum performance.
115+
return value
116+
117+
# Reduce array conditions to scalar boolean using 'any' semantics.
118+
# This ensures the callback can use 'if cond_val:' without ambiguity.
119+
scalar_condition = jnp.asarray(condition).astype(bool)
120+
if scalar_condition.ndim > 0:
121+
scalar_condition = jnp.any(scalar_condition)
122+
123+
def callback(cond_val: Any) -> None:
124+
# Callback to check condition and raise error.
125+
if cond_val:
126+
raise jax.errors.JaxRuntimeError(str(msg))
127+
128+
jax.debug.callback(callback, scalar_condition)
129+
return value

0 commit comments

Comments
 (0)