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.
Files changed (110) hide show
  1. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/PKG-INFO +1 -1
  2. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pyproject.toml +4 -0
  3. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/__init__.py +1 -1
  4. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/_version.py +2 -2
  5. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/activations.py +37 -8
  6. pytensor_ml-0.0.7/pytensor_ml/base.py +56 -0
  7. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/checkpoint.py +15 -18
  8. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/__init__.py +3 -5
  9. pytensor_ml-0.0.7/pytensor_ml/dispatch/jax/__init__.py +3 -0
  10. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/jax/attention.py +1 -1
  11. pytensor_ml-0.0.7/pytensor_ml/dispatch/mlx/__init__.py +3 -0
  12. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/dispatch/mlx/attention.py +1 -1
  13. pytensor_ml-0.0.7/pytensor_ml/json_serialize.py +89 -0
  14. pytensor_ml-0.0.7/pytensor_ml/layers/__init__.py +40 -0
  15. {pytensor_ml-0.0.5/pytensor_ml → pytensor_ml-0.0.7/pytensor_ml/layers}/attention.py +4 -5
  16. pytensor_ml-0.0.7/pytensor_ml/layers/combinators.py +35 -0
  17. pytensor_ml-0.0.7/pytensor_ml/layers/dropout.py +40 -0
  18. pytensor_ml-0.0.7/pytensor_ml/layers/embedding.py +53 -0
  19. pytensor_ml-0.0.7/pytensor_ml/layers/linear.py +58 -0
  20. pytensor_ml-0.0.7/pytensor_ml/layers/norm.py +337 -0
  21. {pytensor_ml-0.0.5/pytensor_ml → pytensor_ml-0.0.7/pytensor_ml/layers}/transformer.py +5 -2
  22. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/loss.py +26 -12
  23. pytensor_ml-0.0.7/pytensor_ml/model.py +128 -0
  24. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/optim/__init__.py +23 -1
  25. pytensor_ml-0.0.7/pytensor_ml/optim/alias.py +233 -0
  26. pytensor_ml-0.0.7/pytensor_ml/optim/base.py +198 -0
  27. pytensor_ml-0.0.7/pytensor_ml/optim/rules.py +586 -0
  28. pytensor_ml-0.0.7/pytensor_ml/optim/train.py +77 -0
  29. pytensor_ml-0.0.7/pytensor_ml/params.py +56 -0
  30. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/pretrained.py +19 -17
  31. pytensor_ml-0.0.7/pytensor_ml/pytensorf/__init__.py +29 -0
  32. pytensor_ml-0.0.7/pytensor_ml/pytensorf/collect.py +95 -0
  33. pytensor_ml-0.0.7/pytensor_ml/pytensorf/compile.py +109 -0
  34. pytensor_ml-0.0.7/pytensor_ml/pytensorf/rewrite.py +41 -0
  35. pytensor_ml-0.0.5/pytensor_ml/pytensorf.py → pytensor_ml-0.0.7/pytensor_ml/pytensorf/rng.py +5 -158
  36. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/layers.py +25 -18
  37. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/__init__.py +2 -2
  38. pytensor_ml-0.0.5/pytensor_ml/json_serialize.py → pytensor_ml-0.0.7/pytensor_ml/serialize/base.py +122 -100
  39. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/blockwise.py +2 -3
  40. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/builders.py +18 -15
  41. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/elemwise.py +5 -5
  42. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scalar.py +20 -13
  43. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/serialize/scan.py +2 -3
  44. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/state.py +22 -35
  45. pytensor_ml-0.0.7/pytensor_ml/util.py +75 -0
  46. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_attention.py +2 -2
  47. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_attention.py +2 -2
  48. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/conftest.py +1 -2
  49. pytensor_ml-0.0.7/tests/optim/test_rules.py +439 -0
  50. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/test_training.py +102 -1
  51. pytensor_ml-0.0.7/tests/rewriting/conftest.py +23 -0
  52. pytensor_ml-0.0.7/tests/rewriting/test_layers.py +52 -0
  53. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_activations.py +63 -5
  54. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_attention.py +1 -1
  55. pytensor_ml-0.0.7/tests/test_base.py +17 -0
  56. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_checkpoint.py +43 -6
  57. pytensor_ml-0.0.7/tests/test_collect.py +82 -0
  58. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_layers.py +106 -57
  59. pytensor_ml-0.0.7/tests/test_loss.py +72 -0
  60. pytensor_ml-0.0.7/tests/test_model.py +102 -0
  61. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_pretrained.py +17 -6
  62. pytensor_ml-0.0.7/tests/test_pytensorf.py +42 -0
  63. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_serialize.py +73 -2
  64. pytensor_ml-0.0.7/tests/test_state.py +93 -0
  65. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/test_transformer.py +2 -2
  66. pytensor_ml-0.0.7/tests/test_util.py +93 -0
  67. pytensor_ml-0.0.5/pytensor_ml/dispatch/jax/__init__.py +0 -3
  68. pytensor_ml-0.0.5/pytensor_ml/dispatch/mlx/__init__.py +0 -3
  69. pytensor_ml-0.0.5/pytensor_ml/layers.py +0 -474
  70. pytensor_ml-0.0.5/pytensor_ml/model.py +0 -91
  71. pytensor_ml-0.0.5/pytensor_ml/optim/alias.py +0 -119
  72. pytensor_ml-0.0.5/pytensor_ml/optim/base.py +0 -112
  73. pytensor_ml-0.0.5/pytensor_ml/optim/rules.py +0 -279
  74. pytensor_ml-0.0.5/pytensor_ml/optim/train.py +0 -60
  75. pytensor_ml-0.0.5/pytensor_ml/params.py +0 -227
  76. pytensor_ml-0.0.5/pytensor_ml/util.py +0 -49
  77. pytensor_ml-0.0.5/tests/optim/test_rules.py +0 -118
  78. pytensor_ml-0.0.5/tests/rewriting/test_layers.py +0 -62
  79. pytensor_ml-0.0.5/tests/test_loss.py +0 -61
  80. pytensor_ml-0.0.5/tests/test_model.py +0 -116
  81. pytensor_ml-0.0.5/tests/test_params.py +0 -107
  82. pytensor_ml-0.0.5/tests/test_pytensorf.py +0 -38
  83. pytensor_ml-0.0.5/tests/test_state.py +0 -34
  84. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.gitattributes +0 -0
  85. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/mypy.yml +0 -0
  86. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/pre-commit.yml +0 -0
  87. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/release.yml +0 -0
  88. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.github/workflows/run_tests.yml +0 -0
  89. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.gitignore +0 -0
  90. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/.pre-commit-config.yaml +0 -0
  91. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/LICENSE +0 -0
  92. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/README.md +0 -0
  93. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml-gpu_jax.yml +0 -0
  94. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/conda_envs/pytensor_ml.yml +0 -0
  95. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/examples/mnist_feed_forward.ipynb +0 -0
  96. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/optim/clipping.py +0 -0
  97. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/optim/transform.py +0 -0
  98. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/pytensor_ml/rewriting/__init__.py +0 -0
  99. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/scripts/run_mypy.py +0 -0
  100. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/__init__.py +0 -0
  101. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/conftest.py +0 -0
  102. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/__init__.py +0 -0
  103. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/jax/__init__.py +0 -0
  104. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/jax/test_basic.py +0 -0
  105. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/mlx/__init__.py +0 -0
  106. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/dispatch/mlx/test_basic.py +0 -0
  107. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/__init__.py +0 -0
  108. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/test_clipping.py +0 -0
  109. {pytensor_ml-0.0.5 → pytensor_ml-0.0.7}/tests/optim/test_transform.py +0 -0
  110. {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.5
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 # noqa: F401 (installs the lazy backend-dispatch registration hook)
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.5'
22
- __version_tuple__ = version_tuple = (0, 0, 5)
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 pytensor_ml.layers import Layer
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, -self.negative_slope * 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
- # float(...) keeps these Python floats: numpy's float64 scalars would upcast a float32 input to
137
- # float64, but a Python float casts by value and preserves the input dtype.
152
+ half = _constant_like(0.5, x)
153
+ one = _constant_like(1.0, x)
138
154
  if self.approximate:
139
- out = 0.5 * x * (1 + pt.tanh(float(np.sqrt(2.0 / np.pi)) * (x + 0.044715 * x**3)))
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
- out = 0.5 * x * (1 + pt.erf(x / float(np.sqrt(2.0))))
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__ = ["GELU", "LeakyReLU", "ReLU", "Sigmoid", "SoftPlus", "Softmax", "Swish", "Tanh"]
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: _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()}
50
46
  save_file(tensors, fspath(path))
51
47
 
52
48
 
53
- def _contiguous(value: np.ndarray) -> np.ndarray:
54
- """Return a C-contiguous array safetensors will accept, preserving the array's rank."""
55
- # np.ascontiguousarray forces ndim >= 1, which would rewrite a rank-0 array (e.g. the step count) to
56
- # shape (1,); this keeps rank-0 arrays rank-0.
57
- return value if value.flags["C_CONTIGUOUS"] else np.array(value, order="C")
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
- 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))
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
- # pytensor auto-loads its own backend dispatches because its linker imports them at compile time -- a
7
- # third-party op can't hook into that, and pytensor exposes no plugin/entry-point system. To get the same
8
- # "activate when the backend is actually used, never on the main import path" behavior, watch for pytensor
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,3 @@
1
+ # Imported for its side effect: registering pytensor_ml's JAX funcify dispatches. One submodule per
2
+ # marker op that gets a kernel, mirroring the layout under pytensor_ml/layers.
3
+ import pytensor_ml.dispatch.jax.attention
@@ -3,7 +3,7 @@ import jax.numpy as jnp
3
3
 
4
4
  from pytensor.link.jax.dispatch import jax_funcify
5
5
 
6
- from pytensor_ml.attention import AttentionLayer
6
+ from pytensor_ml.layers.attention import AttentionLayer
7
7
 
8
8
 
9
9
  @jax_funcify.register(AttentionLayer)
@@ -0,0 +1,3 @@
1
+ # Imported for its side effect: registering pytensor_ml's MLX funcify dispatches. One submodule per
2
+ # marker op that gets a kernel, mirroring the layout under pytensor_ml/layers.
3
+ import pytensor_ml.dispatch.mlx.attention
@@ -4,7 +4,7 @@ import mlx.core as mx
4
4
 
5
5
  from pytensor.link.mlx.dispatch import mlx_funcify
6
6
 
7
- from pytensor_ml.attention import AttentionLayer
7
+ from pytensor_ml.layers.attention import AttentionLayer
8
8
 
9
9
 
10
10
  @mlx_funcify.register(AttentionLayer)
@@ -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.layers import Layer, Linear
8
- from pytensor_ml.pytensorf import UnaryLayerOp
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 to the bottom-right when sq != sk so a partial query
53
- # block (the typical decoding case) sees the whole prefix. Reduces to a plain lower triangle when
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