pytensor-ml 0.0.6__tar.gz → 0.0.7__tar.gz
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.
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/PKG-INFO +1 -1
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/_version.py +2 -2
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/checkpoint.py +14 -5
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/model.py +11 -1
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/train.py +23 -9
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_training.py +81 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_checkpoint.py +42 -4
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.gitattributes +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/mypy.yml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/pre-commit.yml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/release.yml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/run_tests.yml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.gitignore +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.pre-commit-config.yaml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/LICENSE +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/README.md +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml-gpu_jax.yml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml.yml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/examples/mnist_feed_forward.ipynb +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pyproject.toml +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/activations.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/base.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/jax/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/jax/attention.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/mlx/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/mlx/attention.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/json_serialize.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/attention.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/combinators.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/dropout.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/embedding.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/linear.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/norm.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/transformer.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/loss.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/alias.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/base.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/clipping.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/rules.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/transform.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/params.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pretrained.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/collect.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/compile.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/rewrite.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/rng.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/layers.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/base.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/blockwise.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/builders.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/elemwise.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scalar.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scan.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/state.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/util.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/scripts/run_mypy.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/conftest.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/jax/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_attention.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_basic.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/mlx/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_attention.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_basic.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/conftest.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_clipping.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_rules.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_transform.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/rewriting/__init__.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/rewriting/conftest.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/rewriting/test_layers.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_activations.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_attention.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_base.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_collect.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_layers.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_loss.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_model.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_pretrained.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_pytensorf.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_serialize.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_state.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_transformer.py +0 -0
- {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytensor_ml
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: A(nother) deep learning library, built on top of Pytensor
|
|
5
5
|
Project-URL: Homepage, https://github.com/pymc-devs/pytensor_ml
|
|
6
6
|
Project-URL: Source, https://github.com/pymc-devs/pytensor_ml
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
21
|
+
__version__ = version = '0.0.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 7)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from collections.abc import Mapping, Sequence
|
|
2
2
|
from os import fspath
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
6
7
|
|
|
@@ -41,14 +42,21 @@ def save_state(shared_variables: Sequence[SharedVariable], path: str | Path) ->
|
|
|
41
42
|
Destination archive, written verbatim.
|
|
42
43
|
"""
|
|
43
44
|
indexed = _index_by_name(shared_variables)
|
|
44
|
-
tensors = {name:
|
|
45
|
+
tensors = {name: _as_saveable_array(variable.get_value()) for name, variable in indexed.items()}
|
|
45
46
|
save_file(tensors, fspath(path))
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
def
|
|
49
|
-
"""
|
|
49
|
+
def _as_saveable_array(value: Any) -> np.ndarray:
|
|
50
|
+
"""
|
|
51
|
+
Return a value as a C-contiguous numpy array safetensors will accept, preserving its rank.
|
|
52
|
+
|
|
53
|
+
A JIT backend stores its own array type in the shared variables a compiled function updates, so a
|
|
54
|
+
trained model's values are ``jax.Array`` or ``mlx.core.array``; both convert through the array
|
|
55
|
+
protocol.
|
|
56
|
+
"""
|
|
57
|
+
array = np.asarray(value)
|
|
50
58
|
# Not np.ascontiguousarray: it forces ndim >= 1, reshaping a rank-0 array such as the step count.
|
|
51
|
-
return
|
|
59
|
+
return array if array.flags["C_CONTIGUOUS"] else np.array(array, order="C")
|
|
52
60
|
|
|
53
61
|
|
|
54
62
|
def load_state(
|
|
@@ -102,7 +110,8 @@ def load_state(
|
|
|
102
110
|
mismatches = []
|
|
103
111
|
for key, variable in target_by_key.items():
|
|
104
112
|
value = values[key]
|
|
105
|
-
|
|
113
|
+
# Through numpy: a backend array's own dtype object never compares equal to a numpy dtype.
|
|
114
|
+
current = np.asarray(variable.get_value(borrow=True))
|
|
106
115
|
if value.shape != current.shape or value.dtype != current.dtype:
|
|
107
116
|
mismatches.append(
|
|
108
117
|
f" {variable.name!r}: target is {current.dtype} of shape {current.shape}, "
|
|
@@ -63,6 +63,7 @@ class Model:
|
|
|
63
63
|
*,
|
|
64
64
|
loss: TensorVariable | None = None,
|
|
65
65
|
inputs: Sequence[Variable] | None = None,
|
|
66
|
+
extra_outputs: Sequence[Variable] | None = None,
|
|
66
67
|
) -> Function:
|
|
67
68
|
"""
|
|
68
69
|
Compile a one-step training function, either against a supervised target or a prebuilt loss.
|
|
@@ -70,7 +71,7 @@ class Model:
|
|
|
70
71
|
Given ``loss_fn``, builds a target placeholder from the model output with :func:`supervised_loss` and
|
|
71
72
|
the step is called as ``step(X_batch, target_batch)``. Given ``loss`` instead, trains that graph
|
|
72
73
|
directly, which is what an autoencoder or a language-model objective needs -- neither has a target
|
|
73
|
-
separate from its input. Either way the step
|
|
74
|
+
separate from its input. Either way the step applies every update in place.
|
|
74
75
|
|
|
75
76
|
Parameters
|
|
76
77
|
----------
|
|
@@ -87,6 +88,14 @@ class Model:
|
|
|
87
88
|
inputs : sequence of Variable, optional
|
|
88
89
|
Data inputs of the step, in call order. Collected from ``loss`` when omitted. Belongs to the
|
|
89
90
|
prebuilt path; the supervised path derives its own.
|
|
91
|
+
extra_outputs : sequence of Variable, optional
|
|
92
|
+
Diagnostics to return alongside the loss, as in :func:`~pytensor_ml.optim.compile_train`.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
step : Function
|
|
97
|
+
The compiled one-step training function. Returns the loss alone, or ``(loss, *extra_outputs)``
|
|
98
|
+
when diagnostics were requested.
|
|
90
99
|
"""
|
|
91
100
|
if loss_fn is not None:
|
|
92
101
|
if loss is not None or inputs is not None:
|
|
@@ -103,6 +112,7 @@ class Model:
|
|
|
103
112
|
rule,
|
|
104
113
|
parameters=self.weights,
|
|
105
114
|
inputs=inputs,
|
|
115
|
+
extra_outputs=extra_outputs,
|
|
106
116
|
compile_kwargs=compile_kwargs or self._compile_kwargs,
|
|
107
117
|
)
|
|
108
118
|
|
|
@@ -19,6 +19,7 @@ def compile_train(
|
|
|
19
19
|
*,
|
|
20
20
|
parameters: Sequence[Parameter] | None = None,
|
|
21
21
|
inputs: Sequence[Variable] | None = None,
|
|
22
|
+
extra_outputs: Sequence[Variable] | None = None,
|
|
22
23
|
compile_kwargs: dict | None = None,
|
|
23
24
|
) -> Function:
|
|
24
25
|
"""
|
|
@@ -26,8 +27,7 @@ def compile_train(
|
|
|
26
27
|
|
|
27
28
|
Differentiates the loss via ``rule``, applies the resulting updates, folds in any non-trainable state
|
|
28
29
|
updates (such as batch-norm running statistics), and compiles. The parameters and data inputs are
|
|
29
|
-
collected from ``loss`` unless given explicitly.
|
|
30
|
-
inputs to the loss, applying every update in place.
|
|
30
|
+
collected from ``loss`` unless given explicitly.
|
|
31
31
|
|
|
32
32
|
Parameters
|
|
33
33
|
----------
|
|
@@ -38,26 +38,40 @@ def compile_train(
|
|
|
38
38
|
parameters : sequence of shared tensor variable, optional
|
|
39
39
|
Parameters to optimize. Collected from ``loss`` with :func:`collect_trainable_params` when omitted.
|
|
40
40
|
inputs : sequence of Variable, optional
|
|
41
|
-
Data inputs of the compiled function, in call order. Collected from ``loss``
|
|
42
|
-
:func:`collect_data_inputs` when omitted; pass them explicitly when call order matters (e.g.
|
|
43
|
-
before targets).
|
|
41
|
+
Data inputs of the compiled function, in call order. Collected from ``loss`` and ``extra_outputs``
|
|
42
|
+
with :func:`collect_data_inputs` when omitted; pass them explicitly when call order matters (e.g.
|
|
43
|
+
features before targets).
|
|
44
|
+
extra_outputs : sequence of Variable, optional
|
|
45
|
+
Diagnostics to return alongside the loss, such as gradient norms or a batch accuracy. Evaluated in
|
|
46
|
+
the same pass as the gradients, so they see the pre-update parameter values, and they add no
|
|
47
|
+
non-trainable state updates of their own. A random node reached only through an extra output does
|
|
48
|
+
still advance its generator, since :func:`~pytensor_ml.pytensorf.function` threads the next-RNG
|
|
49
|
+
update for every generator the outputs draw from.
|
|
44
50
|
compile_kwargs : dict, optional
|
|
45
51
|
Extra keyword arguments forwarded to the function compiler.
|
|
46
52
|
|
|
47
53
|
Returns
|
|
48
54
|
-------
|
|
49
|
-
Function
|
|
50
|
-
The compiled one-step training function.
|
|
55
|
+
step : Function
|
|
56
|
+
The compiled one-step training function, applying every update in place. Returns the loss alone, or
|
|
57
|
+
``(loss, *extra_outputs)`` when diagnostics were requested.
|
|
51
58
|
"""
|
|
59
|
+
extra_outputs = list(extra_outputs or [])
|
|
60
|
+
|
|
52
61
|
if parameters is None:
|
|
53
62
|
parameters = collect_trainable_params(loss)
|
|
54
63
|
if inputs is None:
|
|
55
|
-
inputs = collect_data_inputs(loss)
|
|
64
|
+
inputs = collect_data_inputs([loss, *extra_outputs])
|
|
56
65
|
|
|
57
66
|
updates: dict[SharedVariable, TensorVariable] = dict(rule(loss, parameters))
|
|
67
|
+
|
|
68
|
+
# Assigned per key rather than merged: SupportsKeysAndGetItem is invariant in its key type, so
|
|
69
|
+
# dict.update rejects the narrower NonTrainableParameter keys.
|
|
58
70
|
for parameter, new_value in collect_non_trainable_updates(loss).items():
|
|
59
71
|
updates[parameter] = new_value
|
|
60
72
|
|
|
61
73
|
require_unique_state_names(updates)
|
|
62
74
|
|
|
63
|
-
|
|
75
|
+
outputs = [loss, *extra_outputs] if extra_outputs else loss
|
|
76
|
+
|
|
77
|
+
return function(list(inputs), outputs, updates=updates, **(compile_kwargs or {}))
|
|
@@ -120,6 +120,87 @@ def test_compile_train_infers_parameters_and_inputs():
|
|
|
120
120
|
assert callable(step)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def test_compile_train_returns_extra_outputs_in_order():
|
|
124
|
+
X = pt.tensor("X", shape=(None, 4))
|
|
125
|
+
prediction = Linear("output", n_in=4, n_out=2)(X)
|
|
126
|
+
parameters = collect_trainable_params(prediction)
|
|
127
|
+
initialize(parameters)
|
|
128
|
+
loss, target = supervised_loss(prediction, SquaredError(), ndim_out=2)
|
|
129
|
+
weight_norm = pt.sqrt(sum((parameter**2).sum() for parameter in parameters))
|
|
130
|
+
|
|
131
|
+
step = compile_train(
|
|
132
|
+
loss,
|
|
133
|
+
sgd(1e-2),
|
|
134
|
+
parameters=parameters,
|
|
135
|
+
inputs=[X, target],
|
|
136
|
+
extra_outputs=[weight_norm, prediction],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
norm_before_step = np.sqrt(sum((parameter.get_value() ** 2).sum() for parameter in parameters))
|
|
140
|
+
|
|
141
|
+
rng = np.random.default_rng(0)
|
|
142
|
+
step_loss, step_norm, step_prediction = step(
|
|
143
|
+
rng.normal(size=(8, 4)).astype(config.floatX), np.zeros((8, 2), dtype=config.floatX)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
assert step_loss.ndim == 0
|
|
147
|
+
assert step_prediction.shape == (8, 2)
|
|
148
|
+
|
|
149
|
+
# Extras are evaluated in the gradient pass, so they see the weights the step started from, not the
|
|
150
|
+
# updated ones.
|
|
151
|
+
np.testing.assert_allclose(step_norm, norm_before_step, rtol=1e-5)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_compile_train_collects_inputs_of_extra_outputs():
|
|
155
|
+
# An extra output may read data the loss never touches; unless those inputs are collected too,
|
|
156
|
+
# compilation raises MissingInputError. Called by name so the test says nothing about collection order.
|
|
157
|
+
X = pt.tensor("X", shape=(None, 4))
|
|
158
|
+
example_weights = pt.tensor("example_weights", shape=(None,))
|
|
159
|
+
prediction = Linear("output", n_in=4, n_out=2)(X)
|
|
160
|
+
initialize(collect_trainable_params(prediction))
|
|
161
|
+
loss, target = supervised_loss(prediction, SquaredError(), ndim_out=2)
|
|
162
|
+
|
|
163
|
+
step = compile_train(loss, sgd(1e-2), extra_outputs=[(example_weights * loss).sum()])
|
|
164
|
+
|
|
165
|
+
rng = np.random.default_rng(0)
|
|
166
|
+
step_loss, weighted_loss = step(
|
|
167
|
+
X=rng.normal(size=(8, 4)).astype(config.floatX),
|
|
168
|
+
target=np.zeros((8, 2), dtype=config.floatX),
|
|
169
|
+
example_weights=np.full(8, 2.0, dtype=config.floatX),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
np.testing.assert_allclose(weighted_loss, step_loss * 16.0, rtol=1e-5)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_compile_train_ignores_updates_of_extra_outputs():
|
|
176
|
+
# Extras are read-only observers: a stateful op reached only through an extra output must not have its
|
|
177
|
+
# write-back folded into the step, or monitoring would silently mutate training state.
|
|
178
|
+
X = pt.tensor("X", shape=(None, 4))
|
|
179
|
+
prediction = Linear("fc", n_in=4, n_out=4)(X)
|
|
180
|
+
monitor = BatchNorm2D("bn", n_in=4)(prediction)
|
|
181
|
+
parameters = collect_trainable_params(prediction)
|
|
182
|
+
initialize(parameters)
|
|
183
|
+
loss, target = supervised_loss(prediction, SquaredError(), ndim_out=2)
|
|
184
|
+
|
|
185
|
+
step = compile_train(
|
|
186
|
+
loss,
|
|
187
|
+
sgd(1e-2),
|
|
188
|
+
parameters=parameters,
|
|
189
|
+
inputs=[X, target],
|
|
190
|
+
extra_outputs=[monitor.mean()],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
running_mean = next(
|
|
194
|
+
p for p in collect_non_trainable_params(monitor) if "running_mean" in p.name
|
|
195
|
+
)
|
|
196
|
+
before = running_mean.get_value().copy()
|
|
197
|
+
|
|
198
|
+
rng = np.random.default_rng(0)
|
|
199
|
+
step(rng.normal(size=(16, 4)).astype(config.floatX), np.zeros((16, 4), dtype=config.floatX))
|
|
200
|
+
|
|
201
|
+
np.testing.assert_allclose(running_mean.get_value(), before)
|
|
202
|
+
|
|
203
|
+
|
|
123
204
|
def test_state_for_requires_named_parameter():
|
|
124
205
|
# An unnamed parameter would leave every state buffer named by its bare slot, so distinct parameters
|
|
125
206
|
# would silently share state at serialization boundaries.
|
|
@@ -45,7 +45,9 @@ def test_round_trip_restores_parameters_and_optimizer_state(tmp_path):
|
|
|
45
45
|
|
|
46
46
|
for _ in range(5): # drive params and optimizer state away from the snapshot
|
|
47
47
|
step(*batch)
|
|
48
|
-
assert any(
|
|
48
|
+
assert any(
|
|
49
|
+
not np.allclose(variable.get_value(), snapshot[variable.name]) for variable in checkpointed
|
|
50
|
+
)
|
|
49
51
|
|
|
50
52
|
load_state(checkpointed, path)
|
|
51
53
|
for variable in checkpointed:
|
|
@@ -54,7 +56,7 @@ def test_round_trip_restores_parameters_and_optimizer_state(tmp_path):
|
|
|
54
56
|
|
|
55
57
|
def test_step_counter_round_trips_exactly(tmp_path):
|
|
56
58
|
# The step counter is a rank-0 int64 array; the round-trip must preserve its rank, dtype, and value
|
|
57
|
-
# (np.ascontiguousarray would silently promote it to shape (1,) on save -- see
|
|
59
|
+
# (np.ascontiguousarray would silently promote it to shape (1,) on save -- see _as_saveable_array).
|
|
58
60
|
_, state, _, _ = build_trained_step()
|
|
59
61
|
counter = next(variable for variable in state if variable.name == "adam/step_count")
|
|
60
62
|
saved = counter.get_value()
|
|
@@ -71,6 +73,42 @@ def test_step_counter_round_trips_exactly(tmp_path):
|
|
|
71
73
|
np.testing.assert_array_equal(restored, saved)
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
def test_non_contiguous_value_round_trips_without_reordering(tmp_path):
|
|
77
|
+
# safetensors serializes an array's raw buffer: handed an F-ordered value it silently writes the
|
|
78
|
+
# elements in the wrong order rather than raising, so the save must copy to C order first.
|
|
79
|
+
variable = shared(np.arange(6, dtype="float64").reshape(2, 3).T, "w")
|
|
80
|
+
assert not variable.get_value(borrow=True).flags["C_CONTIGUOUS"]
|
|
81
|
+
|
|
82
|
+
path = tmp_path / "checkpoint.safetensors"
|
|
83
|
+
save_state([variable], path)
|
|
84
|
+
variable.set_value(np.zeros((3, 2)))
|
|
85
|
+
load_state([variable], path)
|
|
86
|
+
|
|
87
|
+
np.testing.assert_array_equal(variable.get_value(), np.arange(6).reshape(2, 3).T)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.parametrize(
|
|
91
|
+
"mode, backend_module", [("JAX", "jax"), ("MLX", "mlx.core")], ids=["jax", "mlx"]
|
|
92
|
+
)
|
|
93
|
+
def test_round_trip_after_compiled_backend_update(mode, backend_module, tmp_path):
|
|
94
|
+
# A function compiled for a JIT backend stores that backend's array type in the shared variables it
|
|
95
|
+
# updates, so both sides of a checkpoint taken after training see no numpy at all.
|
|
96
|
+
pytest.importorskip(backend_module)
|
|
97
|
+
weight = shared([1.0, 2.0], "w", dtype="float32")
|
|
98
|
+
counter = shared(0, "step", dtype="int32")
|
|
99
|
+
pytensor.function([], [], updates={weight: weight * 2, counter: counter + 1}, mode=mode)()
|
|
100
|
+
assert not isinstance(weight.get_value(), np.ndarray) # the premise: no longer a numpy value
|
|
101
|
+
|
|
102
|
+
path = tmp_path / "checkpoint.safetensors"
|
|
103
|
+
save_state([weight, counter], path)
|
|
104
|
+
weight.set_value(np.zeros(2, dtype="float32"))
|
|
105
|
+
counter.set_value(np.asarray(0, dtype="int32"))
|
|
106
|
+
load_state([weight, counter], path)
|
|
107
|
+
|
|
108
|
+
np.testing.assert_array_equal(weight.get_value(), [2.0, 4.0])
|
|
109
|
+
np.testing.assert_array_equal(counter.get_value(), 1)
|
|
110
|
+
|
|
111
|
+
|
|
74
112
|
def test_name_map_loads_under_renamed_keys(tmp_path):
|
|
75
113
|
saved = shared([1.0, 2.0], "saved/weight")
|
|
76
114
|
path = tmp_path / "checkpoint.safetensors"
|
|
@@ -102,7 +140,7 @@ def test_load_rejects_dtype_mismatch(tmp_path):
|
|
|
102
140
|
# Same shape, different dtype: a real footgun when loading a lower-precision checkpoint into fp64 params.
|
|
103
141
|
save_state([shared([1, 2, 3], "w", dtype="int64")], tmp_path / "c.safetensors")
|
|
104
142
|
target = shared([0.0, 0.0, 0.0], "w")
|
|
105
|
-
with pytest.raises(ValueError, match="
|
|
143
|
+
with pytest.raises(ValueError, match="archive has int64"):
|
|
106
144
|
load_state([target], tmp_path / "c.safetensors")
|
|
107
145
|
np.testing.assert_array_equal(target.get_value(), [0.0, 0.0, 0.0])
|
|
108
146
|
|
|
@@ -148,4 +186,4 @@ def test_parameters_keep_identity_across_compilation():
|
|
|
148
186
|
function([X, target], loss, updates=adam(1e-2)(loss, before))
|
|
149
187
|
|
|
150
188
|
after = collect_trainable_params(prediction)
|
|
151
|
-
assert all(b is a for b, a in zip(before, after))
|
|
189
|
+
assert all(b is a for b, a in zip(before, after, strict=True))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|