pytensor-ml 0.0.5__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.5 → pytensor_ml-0.0.7}/PKG-INFO +1 -1
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pyproject.toml +4 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/__init__.py +1 -1
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/_version.py +2 -2
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/activations.py +37 -8
- pytensor_ml-0.0.7/pytensor_ml/base.py +56 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/checkpoint.py +15 -18
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/__init__.py +3 -5
- pytensor_ml-0.0.7/pytensor_ml/dispatch/jax/__init__.py +3 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/jax/attention.py +1 -1
- pytensor_ml-0.0.7/pytensor_ml/dispatch/mlx/__init__.py +3 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/mlx/attention.py +1 -1
- pytensor_ml-0.0.7/pytensor_ml/json_serialize.py +89 -0
- pytensor_ml-0.0.7/pytensor_ml/layers/__init__.py +40 -0
- {pytensor_ml-0.0.5/pytensor_ml → pytensor_ml-0.0.7/pytensor_ml/layers}/attention.py +4 -5
- pytensor_ml-0.0.7/pytensor_ml/layers/combinators.py +35 -0
- pytensor_ml-0.0.7/pytensor_ml/layers/dropout.py +40 -0
- pytensor_ml-0.0.7/pytensor_ml/layers/embedding.py +53 -0
- pytensor_ml-0.0.7/pytensor_ml/layers/linear.py +58 -0
- pytensor_ml-0.0.7/pytensor_ml/layers/norm.py +337 -0
- {pytensor_ml-0.0.5/pytensor_ml → pytensor_ml-0.0.7/pytensor_ml/layers}/transformer.py +5 -2
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/loss.py +26 -12
- pytensor_ml-0.0.7/pytensor_ml/model.py +128 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/optim/__init__.py +23 -1
- pytensor_ml-0.0.7/pytensor_ml/optim/alias.py +233 -0
- pytensor_ml-0.0.7/pytensor_ml/optim/base.py +198 -0
- pytensor_ml-0.0.7/pytensor_ml/optim/rules.py +586 -0
- pytensor_ml-0.0.7/pytensor_ml/optim/train.py +77 -0
- pytensor_ml-0.0.7/pytensor_ml/params.py +56 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/pretrained.py +19 -17
- pytensor_ml-0.0.7/pytensor_ml/pytensorf/__init__.py +29 -0
- pytensor_ml-0.0.7/pytensor_ml/pytensorf/collect.py +95 -0
- pytensor_ml-0.0.7/pytensor_ml/pytensorf/compile.py +109 -0
- pytensor_ml-0.0.7/pytensor_ml/pytensorf/rewrite.py +41 -0
- pytensor_ml-0.0.5/pytensor_ml/pytensorf.py → pytensor_ml-0.0.7/pytensor_ml/pytensorf/rng.py +5 -158
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/layers.py +25 -18
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/__init__.py +2 -2
- pytensor_ml-0.0.5/pytensor_ml/json_serialize.py → pytensor_ml-0.0.7/pytensor_ml/serialize/base.py +122 -100
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/blockwise.py +2 -3
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/builders.py +18 -15
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/elemwise.py +5 -5
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scalar.py +20 -13
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scan.py +2 -3
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/state.py +22 -35
- pytensor_ml-0.0.7/pytensor_ml/util.py +75 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_attention.py +2 -2
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_attention.py +2 -2
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/conftest.py +1 -2
- pytensor_ml-0.0.7/tests/optim/test_rules.py +439 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/test_training.py +102 -1
- pytensor_ml-0.0.7/tests/rewriting/conftest.py +23 -0
- pytensor_ml-0.0.7/tests/rewriting/test_layers.py +52 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_activations.py +63 -5
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_attention.py +1 -1
- pytensor_ml-0.0.7/tests/test_base.py +17 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_checkpoint.py +43 -6
- pytensor_ml-0.0.7/tests/test_collect.py +82 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_layers.py +106 -57
- pytensor_ml-0.0.7/tests/test_loss.py +72 -0
- pytensor_ml-0.0.7/tests/test_model.py +102 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_pretrained.py +17 -6
- pytensor_ml-0.0.7/tests/test_pytensorf.py +42 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_serialize.py +73 -2
- pytensor_ml-0.0.7/tests/test_state.py +93 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_transformer.py +2 -2
- pytensor_ml-0.0.7/tests/test_util.py +93 -0
- pytensor_ml-0.0.5/pytensor_ml/dispatch/jax/__init__.py +0 -3
- pytensor_ml-0.0.5/pytensor_ml/dispatch/mlx/__init__.py +0 -3
- pytensor_ml-0.0.5/pytensor_ml/layers.py +0 -474
- pytensor_ml-0.0.5/pytensor_ml/model.py +0 -91
- pytensor_ml-0.0.5/pytensor_ml/optim/alias.py +0 -119
- pytensor_ml-0.0.5/pytensor_ml/optim/base.py +0 -112
- pytensor_ml-0.0.5/pytensor_ml/optim/rules.py +0 -279
- pytensor_ml-0.0.5/pytensor_ml/optim/train.py +0 -60
- pytensor_ml-0.0.5/pytensor_ml/params.py +0 -227
- pytensor_ml-0.0.5/pytensor_ml/util.py +0 -49
- pytensor_ml-0.0.5/tests/optim/test_rules.py +0 -118
- pytensor_ml-0.0.5/tests/rewriting/test_layers.py +0 -62
- pytensor_ml-0.0.5/tests/test_loss.py +0 -61
- pytensor_ml-0.0.5/tests/test_model.py +0 -116
- pytensor_ml-0.0.5/tests/test_params.py +0 -107
- pytensor_ml-0.0.5/tests/test_pytensorf.py +0 -38
- pytensor_ml-0.0.5/tests/test_state.py +0 -34
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.gitattributes +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/mypy.yml +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/pre-commit.yml +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/release.yml +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/run_tests.yml +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.gitignore +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.pre-commit-config.yaml +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/LICENSE +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/README.md +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml-gpu_jax.yml +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml.yml +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/examples/mnist_feed_forward.ipynb +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/optim/clipping.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/optim/transform.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/__init__.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/scripts/run_mypy.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/__init__.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/conftest.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/__init__.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/jax/__init__.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_basic.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/mlx/__init__.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_basic.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/__init__.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/test_clipping.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/test_transform.py +0 -0
- {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/rewriting/__init__.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
|
|
@@ -111,6 +111,10 @@ ignore = [
|
|
|
111
111
|
]
|
|
112
112
|
fixable = ["ALL"]
|
|
113
113
|
|
|
114
|
+
[tool.ruff.lint.per-file-ignores]
|
|
115
|
+
# An __init__.py re-exports its package's API and imports submodules for their registration side effects.
|
|
116
|
+
"**/__init__.py" = ["F401"]
|
|
117
|
+
|
|
114
118
|
|
|
115
119
|
[tool.ruff.lint.isort]
|
|
116
120
|
lines-between-types = 1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
|
-
import pytensor_ml.dispatch #
|
|
4
|
+
import pytensor_ml.dispatch # installs the lazy backend-dispatch registration hook
|
|
5
5
|
|
|
6
6
|
from pytensor_ml._version import __version__
|
|
7
7
|
from pytensor_ml.checkpoint import load_state, save_state
|
|
@@ -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,7 +1,23 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pytensor.tensor as pt
|
|
3
3
|
|
|
4
|
-
from
|
|
4
|
+
from pytensor import config
|
|
5
|
+
|
|
6
|
+
from pytensor_ml.base import Layer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _constant_like(value: float, x: pt.TensorVariable) -> pt.TensorVariable:
|
|
10
|
+
"""
|
|
11
|
+
Wrap a scalar so that combining it with ``x`` cannot widen ``x``'s dtype.
|
|
12
|
+
|
|
13
|
+
PyTensor's autocaster types a bare Python float by value, so whether a literal widens its operand
|
|
14
|
+
depends on that value: against a float32 input ``0.5 * x`` stays float32, while ``0.01 * x``
|
|
15
|
+
promotes to float64. Pinning the constant to ``x``'s dtype removes the dependence.
|
|
16
|
+
"""
|
|
17
|
+
dtype = np.dtype(x.dtype)
|
|
18
|
+
# np.finfo maps complex64 -> float32, keeping complex inputs at their own precision.
|
|
19
|
+
dtype = np.finfo(dtype).dtype if np.issubdtype(dtype, np.inexact) else np.dtype(config.floatX)
|
|
20
|
+
return pt.constant(np.asarray(value, dtype=dtype))
|
|
5
21
|
|
|
6
22
|
|
|
7
23
|
class Activation(Layer): ...
|
|
@@ -46,7 +62,7 @@ class LeakyReLU(Activation):
|
|
|
46
62
|
|
|
47
63
|
def __call__(self, x: pt.TensorLike) -> pt.TensorVariable:
|
|
48
64
|
x = pt.as_tensor(x)
|
|
49
|
-
out = pt.switch(x > 0, x,
|
|
65
|
+
out = pt.switch(x > 0, x, _constant_like(self.negative_slope, x) * x)
|
|
50
66
|
out.name = "LeakyReLU"
|
|
51
67
|
return out
|
|
52
68
|
|
|
@@ -133,12 +149,15 @@ class GELU(Activation):
|
|
|
133
149
|
|
|
134
150
|
def __call__(self, x: pt.TensorLike) -> pt.TensorVariable:
|
|
135
151
|
x = pt.as_tensor(x)
|
|
136
|
-
|
|
137
|
-
|
|
152
|
+
half = _constant_like(0.5, x)
|
|
153
|
+
one = _constant_like(1.0, x)
|
|
138
154
|
if self.approximate:
|
|
139
|
-
|
|
155
|
+
sqrt_2_over_pi = _constant_like(np.sqrt(2.0 / np.pi), x)
|
|
156
|
+
cubic_coef = _constant_like(0.044715, x)
|
|
157
|
+
out = half * x * (one + pt.tanh(sqrt_2_over_pi * (x + cubic_coef * x**3)))
|
|
140
158
|
else:
|
|
141
|
-
|
|
159
|
+
sqrt2 = _constant_like(np.sqrt(2.0), x)
|
|
160
|
+
out = half * x * (one + pt.erf(x / sqrt2))
|
|
142
161
|
out.name = "GELU"
|
|
143
162
|
return out
|
|
144
163
|
|
|
@@ -163,7 +182,7 @@ class Swish(Activation):
|
|
|
163
182
|
|
|
164
183
|
def __call__(self, x: pt.TensorLike) -> pt.TensorVariable:
|
|
165
184
|
x = pt.as_tensor(x)
|
|
166
|
-
out = x * pt.sigmoid(self.beta * x)
|
|
185
|
+
out = x * pt.sigmoid(_constant_like(self.beta, x) * x)
|
|
167
186
|
out.name = "Swish"
|
|
168
187
|
return out
|
|
169
188
|
|
|
@@ -193,4 +212,14 @@ class Softmax(Activation):
|
|
|
193
212
|
return out
|
|
194
213
|
|
|
195
214
|
|
|
196
|
-
__all__ = [
|
|
215
|
+
__all__ = [
|
|
216
|
+
"GELU",
|
|
217
|
+
"Activation",
|
|
218
|
+
"LeakyReLU",
|
|
219
|
+
"ReLU",
|
|
220
|
+
"Sigmoid",
|
|
221
|
+
"SoftPlus",
|
|
222
|
+
"Softmax",
|
|
223
|
+
"Swish",
|
|
224
|
+
"Tanh",
|
|
225
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Protocol, runtime_checkable
|
|
3
|
+
|
|
4
|
+
import pytensor.tensor as pt
|
|
5
|
+
|
|
6
|
+
from pytensor.compile.builders import SymbolicOp
|
|
7
|
+
from pytensor.tensor.variable import TensorVariable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Layer(ABC):
|
|
11
|
+
"""Base class for the objects that build layer graphs. Defined here, not in ``pytensor_ml.layers``, so
|
|
12
|
+
that ``pytensor_ml.activations`` can subclass it without a circular import."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def __call__(self, x: pt.TensorLike) -> pt.TensorVariable: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LayerOp(SymbolicOp):
|
|
19
|
+
"""Base class for the library's neural-network ops.
|
|
20
|
+
|
|
21
|
+
A ``SymbolicOp`` is an ``OpFromGraph`` whose inner graph is rebuilt from its ``__props__`` and input
|
|
22
|
+
types by :meth:`build_inner_graph`, so equal props with equal inputs yield an identical op. Basing the
|
|
23
|
+
layers on it (rather than plain ``OpFromGraph``) is what lets the numba backend optimize each inner
|
|
24
|
+
graph: ``SymbolicOp`` restores the fgraph-aware ``__eq__``/``__hash__`` that a props-carrying
|
|
25
|
+
``OpFromGraph`` would otherwise lose, so the ``ofg_inner_graph`` rewrite keeps its optimized inner
|
|
26
|
+
graph instead of discarding it as unchanged.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__props__: tuple[str, ...] = ()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnaryLayerOp(LayerOp):
|
|
33
|
+
"""A ``LayerOp`` with exactly one output, typed as such.
|
|
34
|
+
|
|
35
|
+
``SymbolicOp.__call__`` is annotated ``Variable | list[Variable]`` because an op may produce many
|
|
36
|
+
outputs; a unary layer op produces one, so narrow the result to ``TensorVariable``. The ``isinstance``
|
|
37
|
+
guard narrows for the type checker without a cast and asserts the invariant at runtime.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __call__(self, *inputs, **kwargs) -> TensorVariable:
|
|
41
|
+
out = super().__call__(*inputs, **kwargs)
|
|
42
|
+
assert isinstance(out, TensorVariable), f"{type(self).__name__} produced multiple outputs"
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@runtime_checkable
|
|
47
|
+
class StatefulOp(Protocol):
|
|
48
|
+
"""An op that writes some of its outputs back to shared variables it takes as inputs, the way batch
|
|
49
|
+
norm updates its running statistics. Defining :meth:`update_map` is what marks an op stateful -- the
|
|
50
|
+
check is structural, so there is nothing to register."""
|
|
51
|
+
|
|
52
|
+
def update_map(self) -> dict[int, int]:
|
|
53
|
+
"""Map each output index to the index of the input that output updates."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["Layer", "LayerOp", "StatefulOp", "UnaryLayerOp"]
|
|
@@ -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
|
|
|
@@ -39,22 +40,23 @@ def save_state(shared_variables: Sequence[SharedVariable], path: str | Path) ->
|
|
|
39
40
|
Variables whose values to save. Every variable must have a unique, non-``None`` name.
|
|
40
41
|
path : str or pathlib.Path
|
|
41
42
|
Destination archive, written verbatim.
|
|
42
|
-
|
|
43
|
-
Raises
|
|
44
|
-
------
|
|
45
|
-
ValueError
|
|
46
|
-
If any variable is unnamed or two variables share a name.
|
|
47
43
|
"""
|
|
48
44
|
indexed = _index_by_name(shared_variables)
|
|
49
|
-
tensors = {name:
|
|
45
|
+
tensors = {name: _as_saveable_array(variable.get_value()) for name, variable in indexed.items()}
|
|
50
46
|
save_file(tensors, fspath(path))
|
|
51
47
|
|
|
52
48
|
|
|
53
|
-
def
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
58
|
+
# Not np.ascontiguousarray: it forces ndim >= 1, reshaping a rank-0 array such as the step count.
|
|
59
|
+
return array if array.flags["C_CONTIGUOUS"] else np.array(array, order="C")
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def load_state(
|
|
@@ -80,12 +82,6 @@ def load_state(
|
|
|
80
82
|
Maps a variable's name to the archive key to read it from, for loading a checkpoint saved under
|
|
81
83
|
different names (such as HuggingFace's). The mapping must be injective. Names absent from the map
|
|
82
84
|
are matched directly.
|
|
83
|
-
|
|
84
|
-
Raises
|
|
85
|
-
------
|
|
86
|
-
ValueError
|
|
87
|
-
If any variable is unnamed, two variables share a name, ``name_map`` is not injective, the archive
|
|
88
|
-
keys do not match the target names exactly, or a value's shape or dtype differs from its target.
|
|
89
85
|
"""
|
|
90
86
|
indexed = _index_by_name(shared_variables)
|
|
91
87
|
name_map = name_map or {}
|
|
@@ -114,7 +110,8 @@ def load_state(
|
|
|
114
110
|
mismatches = []
|
|
115
111
|
for key, variable in target_by_key.items():
|
|
116
112
|
value = values[key]
|
|
117
|
-
|
|
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))
|
|
118
115
|
if value.shape != current.shape or value.dtype != current.dtype:
|
|
119
116
|
mismatches.append(
|
|
120
117
|
f" {variable.name!r}: target is {current.dtype} of shape {current.shape}, "
|
|
@@ -3,11 +3,9 @@ import importlib.abc
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# loading a backend's dispatch package and register ours right after. Installing the hook imports nothing
|
|
10
|
-
# heavy; jax/mlx are pulled in only when (and if) that backend compiles a graph.
|
|
6
|
+
# Pytensor imports its own backend dispatches from the linker at compile time and offers no plugin hook, so
|
|
7
|
+
# watch for a backend's dispatch package loading and register ours right after. This keeps jax/mlx off the
|
|
8
|
+
# main import path -- they load only when that backend actually compiles a graph.
|
|
11
9
|
_REGISTRATIONS = {
|
|
12
10
|
"pytensor.link.jax.dispatch": "pytensor_ml.dispatch.jax",
|
|
13
11
|
"pytensor.link.mlx.dispatch": "pytensor_ml.dispatch.mlx",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from pytensor.graph.basic import Variable
|
|
4
|
+
|
|
5
|
+
# Importing the package registers every op-family handler into the codec's dispatch tables. Those
|
|
6
|
+
# handlers depend on serialize.base, never on this module, so the import runs one way.
|
|
7
|
+
import pytensor_ml.serialize # noqa: F401
|
|
8
|
+
|
|
9
|
+
from pytensor_ml.serialize.base import (
|
|
10
|
+
const_from_json,
|
|
11
|
+
const_to_json,
|
|
12
|
+
graph_from_json,
|
|
13
|
+
graph_to_json,
|
|
14
|
+
op_from_json,
|
|
15
|
+
op_to_json,
|
|
16
|
+
prop_from_json,
|
|
17
|
+
prop_to_json,
|
|
18
|
+
qualname,
|
|
19
|
+
register_from_json,
|
|
20
|
+
register_type,
|
|
21
|
+
resolve_class,
|
|
22
|
+
type_from_json,
|
|
23
|
+
type_to_json,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def serialize_graph(inputs: Sequence[Variable], outputs: Sequence[Variable]) -> dict:
|
|
28
|
+
"""
|
|
29
|
+
Serialize a pytensor graph to a JSON-native dict.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
inputs : sequence of Variable
|
|
34
|
+
The graph's inputs, in order. Include every shared variable the graph reads, since the serialized
|
|
35
|
+
form addresses inputs positionally and carries no values.
|
|
36
|
+
outputs : sequence of Variable
|
|
37
|
+
The graph's outputs.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
dict
|
|
42
|
+
A JSON-native description of the graph's structure (no parameter values).
|
|
43
|
+
"""
|
|
44
|
+
return graph_to_json(list(inputs), list(outputs))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def deserialize_graph(
|
|
48
|
+
graph_dict: dict, inputs: Sequence[Variable] | None = None
|
|
49
|
+
) -> tuple[list[Variable], list[Variable]]:
|
|
50
|
+
"""
|
|
51
|
+
Rebuild a pytensor graph from :func:`serialize_graph` output.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
graph_dict : dict
|
|
56
|
+
A graph description produced by :func:`serialize_graph`.
|
|
57
|
+
inputs : sequence of Variable, optional
|
|
58
|
+
Input leaves to build the graph on, in the original order. Supply these to control input identity —
|
|
59
|
+
for example to reattach named shared variables. Fresh, plain symbolic variables are created when
|
|
60
|
+
omitted.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
inputs : list of Variable
|
|
65
|
+
The input variables, in the original order.
|
|
66
|
+
outputs : list of Variable
|
|
67
|
+
The rebuilt graph outputs.
|
|
68
|
+
"""
|
|
69
|
+
return graph_from_json(graph_dict, inputs)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"const_from_json",
|
|
74
|
+
"const_to_json",
|
|
75
|
+
"deserialize_graph",
|
|
76
|
+
"graph_from_json",
|
|
77
|
+
"graph_to_json",
|
|
78
|
+
"op_from_json",
|
|
79
|
+
"op_to_json",
|
|
80
|
+
"prop_from_json",
|
|
81
|
+
"prop_to_json",
|
|
82
|
+
"qualname",
|
|
83
|
+
"register_from_json",
|
|
84
|
+
"register_type",
|
|
85
|
+
"resolve_class",
|
|
86
|
+
"serialize_graph",
|
|
87
|
+
"type_from_json",
|
|
88
|
+
"type_to_json",
|
|
89
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Import from the submodule, never the package: this file runs first, so the package is still incomplete.
|
|
2
|
+
# The marker ops are re-exported because saved graphs name them as ``pytensor_ml.layers.<Op>``.
|
|
3
|
+
from pytensor_ml.base import Layer
|
|
4
|
+
from pytensor_ml.layers.attention import (
|
|
5
|
+
AttentionLayer,
|
|
6
|
+
CausalSelfAttention,
|
|
7
|
+
MultiheadAttention,
|
|
8
|
+
scaled_dot_product_attention,
|
|
9
|
+
)
|
|
10
|
+
from pytensor_ml.layers.combinators import Concatenate, Input, Sequential, Squeeze
|
|
11
|
+
from pytensor_ml.layers.dropout import Dropout, DropoutLayer
|
|
12
|
+
from pytensor_ml.layers.embedding import Embedding, EmbeddingLayer
|
|
13
|
+
from pytensor_ml.layers.linear import Linear, LinearLayer
|
|
14
|
+
from pytensor_ml.layers.norm import (
|
|
15
|
+
BatchNorm2D,
|
|
16
|
+
BatchNormLayer,
|
|
17
|
+
LayerNorm,
|
|
18
|
+
LayerNormLayer,
|
|
19
|
+
NoRunningStatsBatchNormLayer,
|
|
20
|
+
PredictionBatchNormLayer,
|
|
21
|
+
)
|
|
22
|
+
from pytensor_ml.layers.transformer import FeedForward, TransformerBlock
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BatchNorm2D",
|
|
26
|
+
"CausalSelfAttention",
|
|
27
|
+
"Concatenate",
|
|
28
|
+
"Dropout",
|
|
29
|
+
"Embedding",
|
|
30
|
+
"FeedForward",
|
|
31
|
+
"Input",
|
|
32
|
+
"Layer",
|
|
33
|
+
"LayerNorm",
|
|
34
|
+
"Linear",
|
|
35
|
+
"MultiheadAttention",
|
|
36
|
+
"Sequential",
|
|
37
|
+
"Squeeze",
|
|
38
|
+
"TransformerBlock",
|
|
39
|
+
"scaled_dot_product_attention",
|
|
40
|
+
]
|
|
@@ -4,8 +4,8 @@ import pytensor.tensor as pt
|
|
|
4
4
|
from pytensor import config
|
|
5
5
|
from pytensor.tensor.variable import TensorVariable
|
|
6
6
|
|
|
7
|
-
from pytensor_ml.
|
|
8
|
-
from pytensor_ml.
|
|
7
|
+
from pytensor_ml.base import Layer, UnaryLayerOp
|
|
8
|
+
from pytensor_ml.layers.linear import Linear
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class AttentionLayer(UnaryLayerOp):
|
|
@@ -49,9 +49,8 @@ def _sdpa_graph(
|
|
|
49
49
|
|
|
50
50
|
if is_causal:
|
|
51
51
|
sq, sk = q.shape[-2], k.shape[-2]
|
|
52
|
-
# Position i may attend to j <= i, aligned
|
|
53
|
-
#
|
|
54
|
-
# sq == sk
|
|
52
|
+
# Position i may attend to j <= i, aligned bottom-right so a partial query block (the decoding
|
|
53
|
+
# case) sees the whole prefix. Reduces to a plain lower triangle when sq == sk.
|
|
55
54
|
q_idx = pt.arange(sq)[:, None]
|
|
56
55
|
k_idx = pt.arange(sk)[None, :]
|
|
57
56
|
causal = pt.where(k_idx <= q_idx + (sk - sq), 0.0, -np.inf).astype(config.floatX)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
import pytensor.tensor as pt
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def Input(name: str, shape: tuple[int, ...], dtype: str | None = None) -> pt.TensorVariable:
|
|
7
|
+
"""
|
|
8
|
+
Create a named symbolic input tensor with a fully static shape.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
name : str
|
|
13
|
+
Name of the input variable.
|
|
14
|
+
shape : tuple of int
|
|
15
|
+
Static size of each dimension. Raise ``ValueError`` if any entry is not an integer.
|
|
16
|
+
dtype : str or None
|
|
17
|
+
Data type of the input. Defaults to ``floatX`` when None.
|
|
18
|
+
"""
|
|
19
|
+
if not all(isinstance(dim, int) for dim in shape):
|
|
20
|
+
raise ValueError("All dimensions must be integers")
|
|
21
|
+
|
|
22
|
+
return pt.tensor(name=name, shape=shape, dtype=dtype)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def Sequential(*layers: Callable) -> Callable:
|
|
26
|
+
def forward(x: pt.TensorLike) -> pt.TensorLike:
|
|
27
|
+
for layer in layers:
|
|
28
|
+
x = layer(x)
|
|
29
|
+
return x
|
|
30
|
+
|
|
31
|
+
return forward
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
Squeeze = pt.squeeze
|
|
35
|
+
Concatenate = pt.concatenate
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytensor.tensor as pt
|
|
5
|
+
import pytensor.tensor.random as ptr
|
|
6
|
+
|
|
7
|
+
from pytensor import config
|
|
8
|
+
from pytensor.compile.sharedvalue import shared
|
|
9
|
+
|
|
10
|
+
from pytensor_ml.base import Layer, UnaryLayerOp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DropoutLayer(UnaryLayerOp):
|
|
14
|
+
__props__ = ("p",)
|
|
15
|
+
|
|
16
|
+
def build_inner_graph(self, X, mask):
|
|
17
|
+
return [pt.where(mask, ift=X / (1 - self.p), iff=0)]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Dropout(Layer):
|
|
21
|
+
def __init__(self, name: str | None = None, p: float = 0.5, random_state: Any | None = None):
|
|
22
|
+
if p < 0.0 or p > 1.0:
|
|
23
|
+
raise ValueError(f"Dropout probability has to be between 0 and 1, but got {p}")
|
|
24
|
+
self.name = name if name else "Dropout"
|
|
25
|
+
self.p = p
|
|
26
|
+
self.rng = shared(np.random.default_rng(random_state))
|
|
27
|
+
|
|
28
|
+
def __call__(self, X: pt.TensorLike) -> pt.TensorVariable:
|
|
29
|
+
X = pt.as_tensor(X)
|
|
30
|
+
p = pt.as_tensor(self.p, dtype=config.floatX)
|
|
31
|
+
_, mask = ptr.bernoulli(p=1 - p, size=X.shape, rng=self.rng, return_next_rng=True)
|
|
32
|
+
mask = mask.astype(config.floatX)
|
|
33
|
+
|
|
34
|
+
X_masked = DropoutLayer(
|
|
35
|
+
name=f"{self.name}[p = {self.p}]",
|
|
36
|
+
p=self.p,
|
|
37
|
+
)(X, mask)
|
|
38
|
+
X_masked.name = f"{self.name}_output"
|
|
39
|
+
|
|
40
|
+
return X_masked
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytensor.tensor as pt
|
|
3
|
+
|
|
4
|
+
from pytensor import config
|
|
5
|
+
|
|
6
|
+
from pytensor_ml.base import Layer, UnaryLayerOp
|
|
7
|
+
from pytensor_ml.params import trainable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmbeddingLayer(UnaryLayerOp):
|
|
11
|
+
__props__ = ("n_embeddings", "n_features")
|
|
12
|
+
|
|
13
|
+
def build_inner_graph(self, ids, W):
|
|
14
|
+
return [W[ids]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Embedding(Layer):
|
|
18
|
+
r"""
|
|
19
|
+
Lookup-table embedding.
|
|
20
|
+
|
|
21
|
+
Map each integer index to a learned row of the ``(n_embeddings, n_features)`` table,
|
|
22
|
+
appending a trailing feature axis of size ``n_features`` while preserving the shape of the
|
|
23
|
+
index input.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
name : str or None
|
|
28
|
+
Name prefix for the layer's parameters. Defaults to "Embedding" when None.
|
|
29
|
+
n_embeddings : int
|
|
30
|
+
Number of rows in the table -- the number of distinct indices it can map.
|
|
31
|
+
n_features : int
|
|
32
|
+
Size of each embedding row.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, name: str | None, n_embeddings: int, n_features: int):
|
|
36
|
+
self.name = name if name else "Embedding"
|
|
37
|
+
self.n_embeddings = n_embeddings
|
|
38
|
+
self.n_features = n_features
|
|
39
|
+
|
|
40
|
+
W_value = np.zeros((n_embeddings, n_features), dtype=config.floatX)
|
|
41
|
+
self.W = trainable(W_value, f"{self.name}_W")
|
|
42
|
+
|
|
43
|
+
def __call__(self, ids: pt.TensorLike) -> pt.TensorVariable:
|
|
44
|
+
ids = pt.as_tensor(ids)
|
|
45
|
+
|
|
46
|
+
out = EmbeddingLayer(
|
|
47
|
+
name=self.name,
|
|
48
|
+
n_embeddings=self.n_embeddings,
|
|
49
|
+
n_features=self.n_features,
|
|
50
|
+
)(ids, self.W)
|
|
51
|
+
out.name = f"{self.name}_output"
|
|
52
|
+
|
|
53
|
+
return out
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytensor.tensor as pt
|
|
3
|
+
|
|
4
|
+
from pytensor import config
|
|
5
|
+
|
|
6
|
+
from pytensor_ml.base import Layer, UnaryLayerOp
|
|
7
|
+
from pytensor_ml.params import trainable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def shape_to_str(shape):
|
|
11
|
+
inner = ",".join([str(st_dim) if st_dim is not None else "?" for st_dim in shape])
|
|
12
|
+
return f"({inner})"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LinearLayer(UnaryLayerOp):
|
|
16
|
+
__props__ = ("n_in", "n_out", "bias")
|
|
17
|
+
|
|
18
|
+
def build_inner_graph(self, X, W, b=None):
|
|
19
|
+
res = X @ W
|
|
20
|
+
if self.bias:
|
|
21
|
+
res = res + b
|
|
22
|
+
return [res]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Linear(Layer):
|
|
26
|
+
def __init__(self, name: str | None, n_in: int, n_out: int, bias: bool = True):
|
|
27
|
+
self.name = name if name else "Linear"
|
|
28
|
+
self.n_in = n_in
|
|
29
|
+
self.n_out = n_out
|
|
30
|
+
self.bias = bias
|
|
31
|
+
|
|
32
|
+
W_value = np.zeros((n_in, n_out), dtype=config.floatX)
|
|
33
|
+
self.W = trainable(W_value, f"{self.name}_W")
|
|
34
|
+
|
|
35
|
+
if self.bias:
|
|
36
|
+
b_value = np.zeros(n_out, dtype=config.floatX)
|
|
37
|
+
self.b = trainable(b_value, f"{self.name}_b")
|
|
38
|
+
|
|
39
|
+
def __call__(self, X: pt.TensorLike) -> pt.TensorVariable:
|
|
40
|
+
X = pt.as_tensor(X)
|
|
41
|
+
|
|
42
|
+
inputs = [X, self.W]
|
|
43
|
+
if self.bias:
|
|
44
|
+
inputs.append(self.b)
|
|
45
|
+
|
|
46
|
+
input_shape = shape_to_str(X.type.shape)
|
|
47
|
+
output_shape = shape_to_str((X @ self.W).type.shape)
|
|
48
|
+
|
|
49
|
+
ofg = LinearLayer(
|
|
50
|
+
name=f"{self.name}[{input_shape} -> {output_shape}]",
|
|
51
|
+
n_in=self.n_in,
|
|
52
|
+
n_out=self.n_out,
|
|
53
|
+
bias=self.bias,
|
|
54
|
+
)
|
|
55
|
+
out = ofg(*inputs)
|
|
56
|
+
out.name = f"{self.name}_output"
|
|
57
|
+
|
|
58
|
+
return out
|