pipekit-jax¶
JAX / Equinox carrier integration for pipekit. Ships JaxModelOp
(the public counterpart of the in-package
pipekit_train.adapters.equinox.EquinoxModelOp, adding byte-identical
weight round-trip through pipekit_experiment.ModelRegistry via the
registry's weights blob), DiffraxForwardModel (a
pipekit_cycle.ForwardModel behind the [diffrax] extra), and the
adjoint interpreters to_diffrax_adjoint / truncated_scan.
Pytree containers¶
Importing pipekit_jax registers pipekit.Sequential,
pipekit.Graph and JaxModelOp as JAX pytrees, so jax.grad / eqx.filter_grad /
jax.vmap see through a pipeline to the leaves of the operators it
holds.
pipekit_jax.pytree
¶
Register pipekit's composition containers as JAX pytrees.
Importing pipekit_jax registers pipekit.Sequential, pipekit.Graph
and pipekit_jax.JaxModelOp with jax.tree_util, so JAX transformations
see through a pipeline to the leaves of the operators it holds:
Sequential— children are its operators, in order.Graph— children are the node operators in topological order; the topology (inputs, node wiring, outputs) is static aux data that holds no operators or parameters.JaxModelOp— the child is its wrappedeqx.Module.
Without the registration a container is a single opaque leaf, and a
learnable coefficient on an operator inside it is a constant to
jax.grad / eqx.filter_grad. pipekit core stays JAX-free: the
registration is an import side effect of this package, like enabling a
jax.config flag.
What the leaves are depends on the operators. An operator that is
itself a pytree (an eqx.Module subclass, or JaxModelOp via its
module) contributes its own leaves. A plain pipekit.Operator is one opaque
leaf — fine under eqx.filter_jit / eqx.filter_grad (non-array
leaves are static there), but not as a direct argument to
jax.jit.
Caveats:
- Registration is by exact type. Subclasses of
Sequential/Graphare not registered automatically. - Operator sharing is not preserved. An operator instance used by two graph nodes flattens to two children; after an unflatten with new leaves they are two independent objects. (Equinox modules behave the same way.)
- Other containers (
Fanout,Branch,Switch,Cache,Try, the parallel maps) are not registered yet.
Example
import equinox as eqx, jax, jax.numpy as jnp import pipekit_jax # registers the containers from pipekit import Operator, Sequential class Scale(Operator, eqx.Module): ... factor: jax.Array ... def _apply(self, x): ... return self.factor * x pipe = Sequential([Scale(jnp.asarray(2.0)), Scale(jnp.asarray(3.0))]) len(jax.tree.leaves(pipe)) 2
JaxOperator¶
Base class for operators that are both a pipekit.Operator and an
eqx.Module: JSON-safe config, pytree leaves, and the weight-blob
round trip. Use check_jax_operator in a library's test suite to
assert the contract (eager ≡ jit, batching, visible leaves, a config
round trip that keeps learnable fields learnable).
pipekit_jax.operator
¶
JaxOperator — a pipekit Operator that is also an eqx.Module.
Domain libraries (finitevolX, spectraldiffx, somax, …) need operators that are both:
- pipekit operators — dual-mode
__call__(eager / graph node),|composition, and a JSONstate/from_stateround trip; - Equinox modules — array fields are pytree leaves, so the operator
works under
jax.jit/jax.grad/jax.vmap, and config-only fields areeqx.field(static=True).
The two compose because Equinox generates a dataclass __init__ from
the fields, and that is exactly the signature pipekit's ConfigMixin
reads to build get_config(). JaxOperator makes the combination
explicit, coerces array fields to JSON in get_config(), and adds the
WeightsMixin byte round trip.
Writing one::
class Diffuse(JaxOperator):
'''u ↦ u + κ ∇²u on a periodic 1-D grid (κ learnable).'''
dx: float = eqx.field(static=True) # config, not a leaf
kappa: jax.Array = eqx.field(converter=jnp.asarray) # leaf
def _apply(self, u):
lap = (jnp.roll(u, -1) - 2 * u + jnp.roll(u, 1)) / self.dx**2
return u + self.kappa * lap
Learnable fields: give them converter=jnp.asarray. A JSON reload
passes a Python float back into the constructor, and a Python float is
not an inexact array — without the converter the reloaded parameter is
silently treated as a constant by eqx.filter_grad.
pipekit_jax.testing.check_jax_operator catches this.
Compiling: use eqx.filter_jit(op) (or jax.jit on a function that
takes op as an argument). jax.jit(op) hashes the callable, and an
eqx.Module with array fields is unhashable.
JaxOperator
¶
Bases: Operator, WeightsMixin, Module
Base class for pipekit operators whose fields are Equinox fields.
Subclasses declare fields as on any eqx.Module and implement
_apply. Operator comes first in the MRO, so pipekit's
dual-mode __call__ is the one that runs (Equinox defines no
__call__).
- Config:
get_config()is auto-derived from the generated__init__and coerced to JSON builtins (jsonable), soop.statesurvivesjson.dumpsandOperator.from_state. Real numbers, strings, booleans, (nested) lists / tuples and str-keyed dicts of them round-trip, and so do nested operators (theirstaterecords, whichfrom_staterebuilds);op.stateraisesTypeErrorfor complex values, and for array fields whose converter would not restore their dtype (JSON drops dtypes: afloat16field needs a casting converter). Operators holding non-array Python objects (callables, modules without a JSON form) should setforbid_in_yaml = Trueand persist throughserialize_weights/with_weights. - Leaves: inexact-array fields are differentiable; mark
config-only fields
eqx.field(static=True); give learnable fieldsconverter=jnp.asarray(see the module docstring). - Weights:
serialize_weights/with_weights/from_registrypersist the operator's own array leaves.
Source code in packages/pipekit-jax/src/pipekit_jax/operator.py
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
state
property
¶
JSON-serialisable state record, validated for a round trip.
Raises:
| Type | Description |
|---|---|
TypeError
|
if a config value cannot survive |
get_config()
¶
JSON-builtin constructor config (arrays → scalars / lists).
jsonable(value)
¶
Coerce a config value to JSON builtins.
Arrays (JAX or NumPy) become a Python scalar (0-d) or nested lists; tuples (including named tuples) become plain tuples and lists plain lists, converted recursively — JSON drops the subclass anyway; dicts are converted recursively; everything else is returned unchanged.
Source code in packages/pipekit-jax/src/pipekit_jax/operator.py
pipekit_jax.testing
¶
Contract checks for JAX operators built on JaxOperator.
Call check_jax_operator from a library's test suite on every operator
it ships::
def test_contract():
check_jax_operator(Diffuse(dx=0.1, kappa=0.5), jnp.sin(x))
check_jax_operator(op, *inputs, vmap=True)
¶
Assert that op behaves as a JAX-native pipekit operator.
Checks, in order:
opis a pipekitOperatorand aneqx.Module.- Eager ≡ jit:
op(*inputs)equalseqx.filter_jitof the same call, withoppassed as an argument (so its leaves are traced). - Batching (when
vmap): stacking every input array along a new leading axis andjax.vmap-ing gives the stacked eager outputs. Non-array inputs (strings, callables, metadata) are held static. - Leaves visible: every inexact-array field of
opis a leaf ofjax.tree.leaves(op), andeqx.filter_gradof a scalar loss returns a gradient for each of them. - Config round trip (unless
forbid_in_yaml):op.statesurvivesjson.dumps→Operator.from_state; the rebuilt operator has the same number of inexact-array leaves (a learnable field that came back as a Python float fails here), the same array leaves (shape / dtype), and the same outputs. Static fields may change container type (a tuple reloads as a list). For an operator whose config nests operators ({"class", "config"}payloads), the children are taken from the live operator and the rest is rebuilt from JSON, so local fields are still checked. - No
staticfield holds an inexact array (it could never train).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
op
|
Any
|
The operator under test. |
required |
*inputs
|
Any
|
Example positional inputs for |
()
|
vmap
|
bool
|
Also check batching over a new leading axis. |
True
|
Raises:
| Type | Description |
|---|---|
AssertionError
|
on the first violated property. |
Source code in packages/pipekit-jax/src/pipekit_jax/testing.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
pipekit_jax.weights
¶
Shared weight-blob round trip for pipekit-jax operators.
WeightsMixin gives an operator serialize_weights / with_weights
/ from_registry: the array leaves of a pytree are written with
equinox.tree_serialise_leaves and read back into a skeleton — an
instance with the same pytree structure, whose non-array leaves
(activations, dtypes, sizes, static config) cannot be recovered from
bytes alone.
Equinox loads leaves positionally, so a skeleton whose fields are in a
different order, or a different class with the same field names — but
the same number of leaves and compatible shapes — would silently receive
weights it was never trained with. The blob therefore starts with a
signature header (tree_signature): the node types of the tree
(module.QualName of every pytree node, from the root down, with the
structural aux data of non-Equinox nodes such as a pipekit.Graph's
wiring) and the key path, shape and dtype of every array leaf. Only
array leaves are stored; every non-array value comes from the skeleton.
with_weights compares the signature with the skeleton before loading.
Node types are read from the treedef, not its repr, and aux data is
encoded by _stable, so nothing process-specific (memory addresses)
enters the signature.
Blob layout::
b"PKJXW1\n" | header length (8 bytes, little endian) | JSON signature
| equinox.tree_serialise_leaves payload
Blobs without the magic prefix (written before the header existed) are still accepted and loaded positionally.
Two hooks pick which pytree is persisted:
_weights_tree()— the tree to serialise (default:self)._from_weights_tree(tree)— wrap a deserialised tree back into an operator (default: returntree).
JaxModelOp persists its wrapped module; JaxOperator persists
itself.
WeightsMixin
¶
serialize_weights / with_weights / from_registry.
Source code in packages/pipekit-jax/src/pipekit_jax/weights.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | |
from_registry(registry, ref)
¶
Reload weights stored under ref into a copy of self.
The inverse of::
registry.store(trained_op, weights=trained_op.serialize_weights())
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
Any
|
Anything satisfying
|
required |
ref
|
str
|
Hash or tag identifying the stored model. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
A new operator with the persisted weights loaded into a |
Any
|
copy of |
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
ValueError
|
if no weights are stored, or they don't match
|
Source code in packages/pipekit-jax/src/pipekit_jax/weights.py
serialize_weights()
¶
Serialise the persisted tree's array leaves to bytes.
A signature header (key path, shape and dtype of every array
leaf) precedes the equinox.tree_serialise_leaves payload, so
with_weights can refuse a skeleton with a different layout.
Source code in packages/pipekit-jax/src/pipekit_jax/weights.py
with_weights(blob)
¶
Return a new operator with weights loaded from blob.
self is the skeleton / template — its non-array leaves
(activations, dtypes, layer sizes) determine the structure into
which the persisted bytes are loaded. The bytes must come from a
serialize_weights call on an operator with the same pytree
structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blob
|
bytes
|
Bytes produced by |
required |
Returns:
| Type | Description |
|---|---|
Any
|
A fresh operator carrying the persisted weight values. |
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
ValueError
|
if the blob's signature (node types, key paths, shapes, dtypes) differs from the skeleton's, or — for a legacy blob without a signature — if it holds more array leaves than the skeleton requests. |
RuntimeError
|
from newer equinox versions when a
deserialised leaf's shape/dtype doesn't match the
skeleton ( |
Source code in packages/pipekit-jax/src/pipekit_jax/weights.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | |
leaf_signature(tree)
¶
[key path, shape, dtype] of every array leaf of tree, in order.
Source code in packages/pipekit-jax/src/pipekit_jax/weights.py
tree_signature(tree)
¶
Node types plus leaf_signature of tree: what a weight blob records.
Source code in packages/pipekit-jax/src/pipekit_jax/weights.py
ScanCycle and Rollout¶
jax.lax.scan-based time stepping. ScanCycle is the jit-native
counterpart of pipekit_cycle.Cycle — same (carrier, state) contract
and history convention, one scan regardless of n_steps, history
returned by ScanCycle.run. Rollout is the carrier-only form,
returning a trajectory sampled every save_every steps.
checkpoint=True rematerialises in chunks on the backward pass: a
ScanCycle call stores O(√n) carries instead of O(n); ScanCycle.run
and Rollout store one carry per recorded / saved point plus one block,
O(n / stride + stride), so they reach O(√n) only when the stride is
about √n.
Measured on a 2-vCPU CPU VM (5-point diffusion step on a 128² grid):
| steps | compile Cycle (unrolled) |
compile ScanCycle |
grad compile Cycle |
grad compile ScanCycle |
|---|---|---|---|---|
| 10 | 0.19 s | 0.12 s | 2.9 s | 0.26 s |
| 50 | — | — | 356 s | 0.28 s |
| 100 | 1.04 s | 0.14 s | — | — |
| 1000 | 19.0 s | 0.13 s | did not finish in 10 min | — |
| steps | grad temp memory, checkpoint=False |
checkpoint=True |
|---|---|---|
| 100 | 6.6 MiB | 1.7 MiB |
| 1000 | 62.9 MiB | 4.4 MiB |
| 4000 | 250.4 MiB | 8.3 MiB |
pipekit_jax.cycle
¶
ScanCycle and Rollout — jax.lax.scan-based time stepping.
pipekit_cycle.Cycle is carrier-agnostic and steps in a Python for
loop. Under jax.jit that loop unrolls: the traced program, compile
time and memory all grow linearly with n_steps, and
save_history appends to self.history from inside traced code.
That is the right tool for host-side stepping (chunked runs with
monitors between chunks) and the wrong one for anything differentiated
end to end — 4DVar windows, rollout training.
ScanCycle— the JAX specialisation ofCycle: same(carrier, state) -> (carrier, state)contract and the same history convention, onelax.scanregardless ofn_steps, and the history is returned byScanCycle.run, never stored.Rollout— the carrier-only form most PDE use needs:carrier -> trajectorysampled everysave_everysteps.
Both accept a pipekit operator or a pipekit_cycle.ForwardModel as the
step, are JaxOperator s (so learnable leaves of the step are reachable
by eqx.filter_grad), and handle arbitrary pytree carriers — dicts,
eqx.Module states, custom carriers — by stacking with
jax.tree.map.
EnsembleScanCycle
¶
Bases: StatefulOperator, JaxOperator
Run a ScanCycle on every ensemble member at once, with jax.vmap.
The carrier is a pytree whose leaves share a leading member axis of
size M; one compiled program advances all members.
.. code-block:: text
cⱼ ← cycle(cⱼ, s), j = 1 … M (in parallel)
State handling (state_axis):
None(default) — one state shared by all members and treated as read-only: every step of every member reads the incoming state; the state a step returns is discarded (also between the steps of the cycle), and the incoming state is returned unchanged.0— one state per member, stacked on a leading axis like the carrier; each member threads its own state.
Not a drop-in for pipekit_cycle.EnsembleCycle when the step
writes shared state: EnsembleCycle threads one state through
the members sequentially (member j's output state feeds member
j + 1), which a vmap cannot reproduce. Use state_axis=0
or EnsembleCycle for that.
Attributes:
| Name | Type | Description |
|---|---|---|
cycle |
ScanCycle
|
The per-member |
state_axis |
int | None
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in packages/pipekit-jax/src/pipekit_jax/cycle.py
run(carrier, state)
¶
Like ScanCycle.run, per member: histories gain a leading M axis.
Returns (members, state, (carriers, states)) with
carriers shaped [M, n_records, …].
Source code in packages/pipekit-jax/src/pipekit_jax/cycle.py
Rollout
¶
Bases: JaxOperator
Carrier-only rollout: c₀ -> [c₀, c_k, c_2k, …, c_n].
.. code-block:: text
cᵢ₊₁ = step(cᵢ), i = 0 … n_steps - 1
trajectory = (c₀?, c_k, c_2k, …, c_n), k = save_every
The trajectory is stacked along a new leading axis with
jax.tree.map, so any pytree carrier works.
Attributes:
| Name | Type | Description |
|---|---|---|
step |
Any
|
A pipekit operator |
n_steps |
int
|
Number of steps; must be a multiple of |
save_every |
int
|
Record every |
include_initial |
bool
|
Prepend |
checkpoint |
bool
|
Rematerialise each |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Source code in packages/pipekit-jax/src/pipekit_jax/cycle.py
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 | |
ScanCycle
¶
Bases: StatefulOperator, JaxOperator
Apply step_op for n_steps inside one jax.lax.scan.
The jit-native counterpart of pipekit_cycle.Cycle.
.. code-block:: text
(cᵢ₊₁, sᵢ₊₁) = step_op(cᵢ, sᵢ), i = 0 … n_steps - 1
Calling the operator returns (c_n, s_n) exactly like Cycle.
run additionally returns the history, recorded with Cycle's
convention — after step i + 1 for every i with
i % history_stride == 0 — so there are ⌈n_steps / stride⌉
entries.
Attributes:
| Name | Type | Description |
|---|---|---|
step_op |
Any
|
A |
n_steps |
int
|
Number of steps ( |
history_stride |
int
|
Record every |
checkpoint |
bool
|
Rematerialise on the backward pass. The forward pass
keeps the carry only at chunk boundaries — every |
truncate |
int | None
|
If set to |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
cycle = ScanCycle(step_op=model_op, n_steps=1000, history_stride=100) carrier, state = eqx.filter_jit(cycle)(c0, s0) carrier, state, (carriers, states) = eqx.filter_jit(cycle.run)(c0, s0)
Source code in packages/pipekit-jax/src/pipekit_jax/cycle.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | |
run(carrier, state)
¶
Run the cycle and return (carrier, state, (carriers, states)).
carriers / states stack the recorded entries along a new
leading axis of length ⌈n_steps / history_stride⌉ (zero when
n_steps == 0).
Source code in packages/pipekit-jax/src/pipekit_jax/cycle.py
JaxModelOp¶
pipekit_jax.model_op
¶
JaxModelOp — pipekit.Operator wrapper around an eqx.Module.
Closes the v0.1 reproducibility caveat documented in
pipekit-train/docs/design/boundaries.md §13.1: the in-package
EquinoxModelOp stand-in shipped with v0.1 couldn't round-trip
trained weights through a ModelRegistry from JSON config alone,
because an eqx.Module is a JAX PyTree of arrays whose values
aren't recoverable from a class name.
JaxModelOp adds two methods on top of the same wrapper surface:
serialize_weights() -> bytes— a leaf-signature header pluseqx.tree_serialise_leavesof the wrapped module's array leaves. Suitable for the registry'sweightsblob.with_weights(blob: bytes) -> JaxModelOp— load the bytes back into the module's array leaves, returning a newJaxModelOpwhosemoduleis structurally identical toself.modulebut with the persisted weight values.selfis the skeleton / template.
The skeleton-from-template requirement is honest: an eqx.Module's
non-array leaves (activation functions, dtypes, hidden-size ints) are
Python objects not reproducible from JSON alone. The user always
needs to build a fresh module of the same shape before calling
with_weights. from_registry packages the
"reload weights from a stored hash into self" pattern into one call.
JaxModelOp
¶
Bases: WeightsMixin, Operator
Wrap an eqx.Module as a pipekit.Operator with weight round-trip.
Drop-in replacement for the in-package
pipekit_train.adapters.equinox.EquinoxModelOp stand-in.
Same constructor signature; same _apply shape (calls the
module on the input); additionally supports
serialize_weights / with_weights for byte-identical
persistence through any pipekit_experiment.ModelRegistry.
The wrapper is marked forbid_in_yaml = True because an
eqx.Module is a PyTree of arrays — the structural config
round-trips (we surface module_class in get_config) but
the array values do not. The round-trip path is:
registry.store(op, weights=op.serialize_weights()) to
persist, then template.with_weights(blob) to reload into
a fresh skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module
|
Module
|
An |
required |
Source code in packages/pipekit-jax/src/pipekit_jax/model_op.py
Adjoint interpreters¶
JAX realisations of the pipekit_cycle.adjoints specs: truncated_scan
implements TruncatedAdjoint at the cycle layer (a jax.lax.scan whose
carry is stop_gradient-ed outside the trailing-k window), and
to_diffrax_adjoint maps specs onto diffrax.AbstractAdjoint instances
for the dynamics layer.
pipekit_jax.adjoints
¶
JAX interpreters for the pipekit adjoint-strategy specs.
Two pieces:
truncated_scan— ajax.lax.scanwhose carry runs understop_gradientexcept within the trailingksteps. The execution engine forTruncatedAdjointat the cycle layer.to_diffrax_adjoint— map a spec frompipekit_cycle.adjointsonto the correspondingdiffrax.AbstractAdjoint(requires thepipekit-jax[diffrax]extra).
Specs are matched by class name and fields rather than identity, so structurally identical spec objects defined by algorithm libraries (which may not depend on pipekit) are accepted interchangeably.
to_diffrax_adjoint(spec)
¶
Map a pipekit adjoint spec onto a diffrax.AbstractAdjoint.
Mapping (specs matched structurally by class name):
RecursiveCheckpointAdjoint(checkpoints)→diffrax.RecursiveCheckpointAdjoint(checkpoints)— the recommended default.DirectAdjoint()→diffrax.DirectAdjoint().BacksolveAdjoint()→diffrax.BacksolveAdjoint(). Warning: continuous adjoints diverge for chaotic or stiff dynamics; never use as a default.ImplicitAdjoint()→diffrax.ImplicitAdjoint()— steady-state solves only.TruncatedAdjoint→ValueError: truncation is a property of an outer rollout (cycle or solver iteration), not of a single ODE/SDE solve; usetruncated_scanat that layer instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
Any
|
A spec instance from |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The corresponding diffrax adjoint instance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
for |
ModuleNotFoundError
|
if diffrax is not installed (install the
|
Source code in packages/pipekit-jax/src/pipekit_jax/adjoints.py
truncated_scan(f, init, xs, *, k=1)
¶
jax.lax.scan with gradients truncated to the trailing k steps.
The carry leaving every step before T - k is wrapped in
stop_gradient, so reverse-mode differentiation sees only the
last k transitions through the carry. Each step's own output
y_t still receives gradients with respect to that step's
inputs — only the flow through history is cut, which is exactly
the ROAD-EnKF / truncated-BPTT training regime. k >= T is the
identity transformation (a plain scan).
Memory for the backward pass is O(k) in the rollout length instead
of O(T), and for chaotic step functions the truncation acts as
gradient regularisation (the exact adjoint norm grows like
exp(lambda_max * T)).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
Callable[[Carry, X], tuple[Carry, Y]]
|
Scan body |
required |
init
|
Carry
|
Initial carry. |
required |
xs
|
X
|
Stacked per-step inputs with leading axis |
required |
k
|
int
|
Number of trailing steps that propagate gradients through the carry. Must be at least 1. |
1
|
Returns:
| Type | Description |
|---|---|
Carry
|
|
Y
|
the forward values are identical; only gradients differ. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Source code in packages/pipekit-jax/src/pipekit_jax/adjoints.py
DiffraxForwardModel¶
A pipekit_cycle.ForwardModel backed by diffrax.diffeqsolve — ODE or
SDE — that also satisfies SupportsAdjoint, so the gradient strategy of
the forecast step is configuration. Requires the pipekit-jax[diffrax]
extra.
pipekit_jax.diffrax_model
¶
DiffraxForwardModel — a pipekit ForwardModel backed by diffrax.
Wraps diffrax.diffeqsolve so a vector field (ODE) or drift +
diffusion pair (SDE) becomes a component that satisfies both
pipekit_cycle.ForwardModel (step(state, dt) plus dt /
state_signature) and pipekit_cycle.SupportsAdjoint
structurally. The gradient strategy for the solve is declared with a
spec from pipekit_cycle.adjoints and interpreted by
pipekit_jax.to_diffrax_adjoint — so swapping, say, checkpointed for
backsolve adjoints is configuration, not code.
Requires the pipekit-jax[diffrax] extra.
DiffraxForwardModel
¶
Bases: Module
Advance a state with diffrax.diffeqsolve.
ODE by default; supply diffusion (and sde_key) for an SDE,
in which case the Brownian path is a VirtualBrownianTree over
each integration span. The adjoint spec selects how reverse-mode
gradients flow through the solve (default: treeverse
checkpointing, the robust choice — see
pipekit_cycle.adjoints for the menu and the warning on
BacksolveAdjoint).
pipekit cycles call step(state, dt) with no absolute time, so
integration always runs over [0, dt] (autonomous dynamics);
as_dynamics() exposes the explicit (state, t0, t1) form
used by algorithm libraries (e.g. filterax AbstractDynamics).
Attributes:
| Name | Type | Description |
|---|---|---|
vector_field |
Callable[[Any, Any, Any], Any]
|
Drift |
solver |
AbstractSolver
|
Any diffrax solver ( |
dt0 |
float
|
Initial/fixed integrator step. |
dt |
float
|
Default window length advertised to pipekit cycles. |
adjoint |
Any
|
Adjoint spec (or a raw |
stepsize_controller |
AbstractStepSizeController
|
diffrax step-size controller; defaults to constant steps. |
diffusion |
Callable[[Any, Any, Any], Any] | None
|
Optional |
brownian_shape |
tuple[int, ...] | None
|
Shape of the Brownian increment (SDE only).
|
sde_key |
Array | None
|
PRNG key for the Brownian path (SDE only). The key is
fixed at construction; refresh it per cycle through your
DA state (e.g. |
max_steps |
int
|
Bound on integrator steps per solve. |
Source code in packages/pipekit-jax/src/pipekit_jax/diffrax_model.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
state_signature
property
¶
No shape contract advertised; pipekit treats None as opt-out.
as_dynamics()
¶
Return the (state, t0, t1) -> state callable form.
Plugs directly into APIs that take explicit integration spans,
e.g. filterax AbstractDynamics slots or vardax 4D-Var
forward arguments.
Source code in packages/pipekit-jax/src/pipekit_jax/diffrax_model.py
step(state, dt)
¶
Advance state by dt (autonomous: integrates [0, dt]).