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.
Files changed (93) hide show
  1. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/PKG-INFO +1 -1
  2. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/_version.py +2 -2
  3. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/checkpoint.py +14 -5
  4. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/model.py +11 -1
  5. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/train.py +23 -9
  6. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_training.py +81 -0
  7. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_checkpoint.py +42 -4
  8. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.gitattributes +0 -0
  9. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/mypy.yml +0 -0
  10. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/pre-commit.yml +0 -0
  11. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/release.yml +0 -0
  12. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.github/workflows/run_tests.yml +0 -0
  13. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.gitignore +0 -0
  14. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/.pre-commit-config.yaml +0 -0
  15. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/LICENSE +0 -0
  16. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/README.md +0 -0
  17. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml-gpu_jax.yml +0 -0
  18. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml.yml +0 -0
  19. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/examples/mnist_feed_forward.ipynb +0 -0
  20. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pyproject.toml +0 -0
  21. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/__init__.py +0 -0
  22. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/activations.py +0 -0
  23. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/base.py +0 -0
  24. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/__init__.py +0 -0
  25. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/jax/__init__.py +0 -0
  26. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/jax/attention.py +0 -0
  27. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/mlx/__init__.py +0 -0
  28. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/mlx/attention.py +0 -0
  29. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/json_serialize.py +0 -0
  30. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/__init__.py +0 -0
  31. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/attention.py +0 -0
  32. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/combinators.py +0 -0
  33. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/dropout.py +0 -0
  34. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/embedding.py +0 -0
  35. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/linear.py +0 -0
  36. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/norm.py +0 -0
  37. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/layers/transformer.py +0 -0
  38. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/loss.py +0 -0
  39. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/__init__.py +0 -0
  40. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/alias.py +0 -0
  41. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/base.py +0 -0
  42. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/clipping.py +0 -0
  43. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/rules.py +0 -0
  44. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/optim/transform.py +0 -0
  45. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/params.py +0 -0
  46. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pretrained.py +0 -0
  47. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/__init__.py +0 -0
  48. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/collect.py +0 -0
  49. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/compile.py +0 -0
  50. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/rewrite.py +0 -0
  51. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/pytensorf/rng.py +0 -0
  52. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/__init__.py +0 -0
  53. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/layers.py +0 -0
  54. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/__init__.py +0 -0
  55. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/base.py +0 -0
  56. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/blockwise.py +0 -0
  57. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/builders.py +0 -0
  58. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/elemwise.py +0 -0
  59. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scalar.py +0 -0
  60. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scan.py +0 -0
  61. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/state.py +0 -0
  62. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/pytensor_ml/util.py +0 -0
  63. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/scripts/run_mypy.py +0 -0
  64. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/__init__.py +0 -0
  65. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/conftest.py +0 -0
  66. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/__init__.py +0 -0
  67. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/jax/__init__.py +0 -0
  68. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_attention.py +0 -0
  69. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_basic.py +0 -0
  70. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/mlx/__init__.py +0 -0
  71. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_attention.py +0 -0
  72. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_basic.py +0 -0
  73. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/__init__.py +0 -0
  74. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/conftest.py +0 -0
  75. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_clipping.py +0 -0
  76. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_rules.py +0 -0
  77. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/optim/test_transform.py +0 -0
  78. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/rewriting/__init__.py +0 -0
  79. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/rewriting/conftest.py +0 -0
  80. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/rewriting/test_layers.py +0 -0
  81. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_activations.py +0 -0
  82. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_attention.py +0 -0
  83. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_base.py +0 -0
  84. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_collect.py +0 -0
  85. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_layers.py +0 -0
  86. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_loss.py +0 -0
  87. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_model.py +0 -0
  88. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_pretrained.py +0 -0
  89. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_pytensorf.py +0 -0
  90. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_serialize.py +0 -0
  91. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_state.py +0 -0
  92. {pytensor_ml-0.0.6 → pytensor_ml-0.0.7}/tests/test_transformer.py +0 -0
  93. {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.6
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.6'
22
- __version_tuple__ = version_tuple = (0, 0, 6)
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: _contiguous(variable.get_value()) for name, variable in indexed.items()}
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 _contiguous(value: np.ndarray) -> np.ndarray:
49
- """Return a C-contiguous array safetensors will accept, preserving the array's rank."""
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 value if value.flags["C_CONTIGUOUS"] else np.array(value, order="C")
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
- current = variable.get_value(borrow=True)
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 returns the loss and applies every update in place.
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. Returns a plain compiled function that maps a batch of
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`` with
42
- :func:`collect_data_inputs` when omitted; pass them explicitly when call order matters (e.g. features
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
- return function(list(inputs), loss, updates=updates, **(compile_kwargs or {}))
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(not np.allclose(v.get_value(), snapshot[v.name]) for v in checkpointed)
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 checkpoint._contiguous).
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="do not match their targets"):
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