pytorchmlx 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.5
2
+ Name: pytorchmlx
3
+ Version: 0.0.1
4
+ Summary: An educational PyTorch-shaped interface for MLX and PyTorch
5
+ Author: Priyanshu Jain
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Priyanshu Jain
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: mlx<0.33,>=0.32.2
30
+ Requires-Dist: numpy
31
+ Requires-Dist: torch<3,>=2.4
32
+ Description-Content-Type: text/markdown
33
+
34
+ # torchmlx
35
+
36
+ torchmlx is a pytorch-shaped compatibility layer that uses mlx on apple silicon and pytorch elsewhere.
37
+
38
+ NOTE: it is experimental and built first for educational use. If you find a bug, please create an issue on the repo.
39
+
40
+ ```python
41
+ import torchmlx as torch
42
+ from torchmlx import nn, optim
43
+
44
+ model = nn.Linear(4, 2)
45
+ optimizer = optim.AdamW(model.parameters(), lr=3e-4)
46
+ ```
47
+
48
+ see the [tinystories example](examples/tinystories-llm/train.py) and [compatibility details](docs/compatibility.md).
@@ -0,0 +1,12 @@
1
+ torchmlx/__init__.py,sha256=Vo3_qG5ib3rDOKUT1N6y7Fr4760MM_yecWUirIBPunQ,8851
2
+ torchmlx/_autograd.py,sha256=HkkesMlbAvFnHc47AenA4Sapp2A7jpXSIOX9u5XJoXc,3866
3
+ torchmlx/_backend.py,sha256=EPBqEX402BLx1DjZ7Ay0N7IAds85GxVZQgSOdZn0qyE,720
4
+ torchmlx/_mlx_tensor.py,sha256=ePvQltx5Oh9_vDFetO5AnMxE58h9ViwBzo_z6GRyZv4,6936
5
+ torchmlx/trainer.py,sha256=CQeNKQfZu-DZ6bQ8InTAQZBAZMOS96HEitW-q8J3J8M,1837
6
+ torchmlx/nn/__init__.py,sha256=QKw7NH8dWyovShPY-SOnZrk3nGG5Gh_DXreSUuIXQr4,8510
7
+ torchmlx/nn/functional.py,sha256=PuGF28lapTv_68x7ejvNoOPjGptcWNWijMoiCi3Zpnw,4457
8
+ torchmlx/optim/__init__.py,sha256=nSX-wNGAfz6DaGzuNVSEnGFuzq25OrfmdGguM5OQnek,2088
9
+ pytorchmlx-0.0.1.dist-info/METADATA,sha256=w3SNzTxmyQ0bmcTzyjtXveFux17N6k-Z_DiTnDmE5bw,2056
10
+ pytorchmlx-0.0.1.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
11
+ pytorchmlx-0.0.1.dist-info/licenses/LICENSE,sha256=Rc24jdpcK087K5-G3PHFZ8AFoZ32KT95EyqvTMvWAQQ,1071
12
+ pytorchmlx-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Priyanshu Jain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
torchmlx/__init__.py ADDED
@@ -0,0 +1,305 @@
1
+ import builtins as _builtins
2
+ import math as _math
3
+ import platform as _platform
4
+ from types import SimpleNamespace as _SimpleNamespace
5
+
6
+ from ._backend import BACKEND, unsupported
7
+
8
+
9
+ def current_backend():
10
+ return BACKEND
11
+
12
+
13
+ if BACKEND == "torch":
14
+ import torch as _native
15
+
16
+ Tensor = _native.Tensor
17
+ tensor = _native.tensor
18
+ from_numpy = _native.from_numpy
19
+ arange = _native.arange
20
+ randint = _native.randint
21
+ chunk = _native.chunk
22
+ transpose = _native.transpose
23
+ float16 = _native.float16
24
+ float32 = _native.float32
25
+ float64 = _native.float64
26
+ bfloat16 = _native.bfloat16
27
+ int8 = _native.int8
28
+ int16 = _native.int16
29
+ int32 = _native.int32
30
+ int64 = _native.int64
31
+ uint8 = _native.uint8
32
+ bool = _native.bool
33
+ half = float16
34
+ float = float32
35
+ double = float64
36
+ int = int32
37
+ long = int64
38
+
39
+ def categorical(logits, dim=-1, num_samples=1):
40
+ probabilities = _native.softmax(logits, dim=dim)
41
+ return _native.multinomial(probabilities, num_samples=num_samples)
42
+
43
+ def __getattr__(name):
44
+ return getattr(_native, name)
45
+
46
+ else:
47
+ import mlx.core as _native
48
+ import numpy as _np
49
+
50
+ Tensor = _native.array
51
+ float16 = _native.float16
52
+ float32 = _native.float32
53
+ float64 = _native.float64
54
+ bfloat16 = _native.bfloat16
55
+ int8 = _native.int8
56
+ int16 = _native.int16
57
+ int32 = _native.int32
58
+ int64 = _native.int64
59
+ uint8 = _native.uint8
60
+ bool = _native.bool_
61
+ half = float16
62
+ float = float32
63
+ double = float64
64
+ int = int32
65
+ long = int64
66
+
67
+ pi = _math.pi
68
+
69
+ class device:
70
+ def __init__(self, value):
71
+ if isinstance(value, device):
72
+ value = value.type
73
+ value = str(value)
74
+ if value != "mps":
75
+ raise ValueError("the MLX backend only accepts device='mps'")
76
+ self.type = value
77
+ self.index = None
78
+
79
+ def __str__(self):
80
+ return self.type
81
+
82
+ def __repr__(self):
83
+ return f"device(type={self.type!r})"
84
+
85
+ def __eq__(self, other):
86
+ return str(other) == self.type
87
+
88
+ class _MPS:
89
+ @staticmethod
90
+ def is_available():
91
+ return _platform.system() == "Darwin" and _platform.machine() == "arm64"
92
+
93
+ class _CUDA:
94
+ @staticmethod
95
+ def is_available():
96
+ return False
97
+
98
+ backends = _SimpleNamespace(mps=_MPS())
99
+ cuda = _CUDA()
100
+
101
+ from ._mlx_tensor import install as _install_tensor_methods
102
+
103
+ _install_tensor_methods(device)
104
+
105
+ def _check_device(device):
106
+ if device is not None and str(device) != "mps":
107
+ raise ValueError("the MLX backend only accepts device='mps'")
108
+
109
+ def tensor(data, dtype=None, device=None, requires_grad=False, pin_memory=False):
110
+ _check_device(device)
111
+ if requires_grad:
112
+ raise RuntimeError(
113
+ "requires_grad is not supported by the MLX backend; use torchmlx.Trainer"
114
+ )
115
+ if pin_memory:
116
+ raise RuntimeError("pin_memory is not supported by the MLX backend")
117
+ if dtype is None and not isinstance(data, (_native.array, _np.ndarray)):
118
+ kind = _np.asarray(data).dtype.kind
119
+ if kind in {"i", "u"}:
120
+ dtype = int64
121
+ elif kind == "b":
122
+ dtype = bool
123
+ return _native.array(data, dtype=dtype)
124
+
125
+ def from_numpy(array):
126
+ if not isinstance(array, _np.ndarray):
127
+ raise TypeError("from_numpy expects a numpy.ndarray")
128
+ return _native.array(array)
129
+
130
+ def arange(start, end=None, step=1, *, dtype=None, device=None):
131
+ _check_device(device)
132
+ if end is None:
133
+ start, end = 0, start
134
+ if dtype is None and all(
135
+ isinstance(value, _builtins.int) for value in (start, end, step)
136
+ ):
137
+ dtype = int64
138
+ return _native.arange(start, end, step, dtype=dtype)
139
+
140
+ def randint(low, high, size, *, dtype=None, device=None, requires_grad=False):
141
+ _check_device(device)
142
+ if requires_grad:
143
+ raise RuntimeError(
144
+ "requires_grad is not supported by the MLX backend; use torchmlx.Trainer"
145
+ )
146
+ result = _native.random.randint(low, high, shape=size)
147
+ return result.astype(dtype or int64)
148
+
149
+ def chunk(input, chunks, dim=0):
150
+ return input.chunk(chunks, dim=dim)
151
+
152
+ def transpose(input, dim0, dim1):
153
+ return input.transpose(dim0, dim1)
154
+
155
+ def zeros(*size, dtype=None, device=None, requires_grad=False):
156
+ _check_device(device)
157
+ if requires_grad:
158
+ raise RuntimeError(
159
+ "requires_grad is not supported by the MLX backend; use torchmlx.Trainer"
160
+ )
161
+ shape = size[0] if len(size) == 1 and isinstance(size[0], (tuple, list)) else size
162
+ return _native.zeros(shape, dtype=dtype or float32)
163
+
164
+ def ones(*size, dtype=None, device=None, requires_grad=False):
165
+ _check_device(device)
166
+ if requires_grad:
167
+ raise RuntimeError(
168
+ "requires_grad is not supported by the MLX backend; use torchmlx.Trainer"
169
+ )
170
+ shape = size[0] if len(size) == 1 and isinstance(size[0], (tuple, list)) else size
171
+ return _native.ones(shape, dtype=dtype or float32)
172
+
173
+ def zeros_like(input, *, dtype=None, device=None, requires_grad=False):
174
+ _check_device(device)
175
+ if requires_grad:
176
+ raise RuntimeError(
177
+ "requires_grad is not supported by the MLX backend; use torchmlx.Trainer"
178
+ )
179
+ return _native.zeros_like(input).astype(dtype or input.dtype)
180
+
181
+ def ones_like(input, *, dtype=None, device=None, requires_grad=False):
182
+ _check_device(device)
183
+ if requires_grad:
184
+ raise RuntimeError(
185
+ "requires_grad is not supported by the MLX backend; use torchmlx.Trainer"
186
+ )
187
+ return _native.ones_like(input).astype(dtype or input.dtype)
188
+
189
+ def cat(tensors, dim=0):
190
+ return _native.concatenate(tensors, axis=dim)
191
+
192
+ def softmax(input, dim, dtype=None):
193
+ value = input.astype(dtype) if dtype is not None else input
194
+ return _native.softmax(value, axis=dim)
195
+
196
+ def triu(input, diagonal=0):
197
+ return _native.triu(input, k=diagonal)
198
+
199
+ def tril(input, diagonal=0):
200
+ return _native.tril(input, k=diagonal)
201
+
202
+ def topk(input, k, dim=None, largest=True, sorted=True):
203
+ axis = -1 if dim is None else dim
204
+ indices = _native.argsort(input, axis=axis)
205
+ indices = _native.flip(indices, axis=axis) if largest else indices
206
+ slices = [slice(None)] * input.ndim
207
+ slices[axis] = slice(0, k)
208
+ indices = indices[tuple(slices)].astype(int64)
209
+ values = _native.take_along_axis(input, indices, axis=axis)
210
+ return values, indices
211
+
212
+ def unique(input, sorted=True, return_inverse=False, return_counts=False, dim=None):
213
+ if return_inverse or return_counts or dim is not None:
214
+ unsupported("torchmlx.unique with non-default options")
215
+ values = _native.sort(input.reshape(-1))
216
+ if values.shape[0] < 2:
217
+ return values
218
+ keep = _native.concatenate(
219
+ [_native.array([True]), values[1:] != values[:-1]]
220
+ )
221
+ return values[keep]
222
+
223
+ exp = _native.exp
224
+ sin = _native.sin
225
+ cos = _native.cos
226
+ tanh = _native.tanh
227
+ sqrt = _native.sqrt
228
+ matmul = _native.matmul
229
+ outer = _native.outer
230
+
231
+ def pow(input, exponent):
232
+ return _native.power(input, exponent)
233
+
234
+ def polar(abs, angle):
235
+ return abs * _native.exp(_native.array(1j) * angle)
236
+
237
+ def categorical(logits, dim=-1, num_samples=1):
238
+ if num_samples == 1:
239
+ return _native.random.categorical(logits, axis=dim)[..., None].astype(int64)
240
+ return _native.random.categorical(
241
+ logits, axis=dim, num_samples=num_samples
242
+ ).astype(int64)
243
+
244
+ def __getattr__(name):
245
+ unsupported(f"torchmlx.{name}")
246
+
247
+
248
+ import importlib as _importlib
249
+
250
+ nn = _importlib.import_module("torchmlx.nn")
251
+ optim = _importlib.import_module("torchmlx.optim")
252
+ from .trainer import Trainer
253
+
254
+ __all__ = [
255
+ "Tensor",
256
+ "Trainer",
257
+ "arange",
258
+ "backends",
259
+ "bfloat16",
260
+ "bool",
261
+ "categorical",
262
+ "cat",
263
+ "chunk",
264
+ "cuda",
265
+ "current_backend",
266
+ "device",
267
+ "double",
268
+ "float",
269
+ "float16",
270
+ "float32",
271
+ "float64",
272
+ "exp",
273
+ "from_numpy",
274
+ "int8",
275
+ "int16",
276
+ "int32",
277
+ "int64",
278
+ "half",
279
+ "int",
280
+ "long",
281
+ "matmul",
282
+ "nn",
283
+ "ones",
284
+ "ones_like",
285
+ "optim",
286
+ "outer",
287
+ "pi",
288
+ "polar",
289
+ "pow",
290
+ "randint",
291
+ "tensor",
292
+ "softmax",
293
+ "sin",
294
+ "cos",
295
+ "sqrt",
296
+ "tanh",
297
+ "topk",
298
+ "transpose",
299
+ "tril",
300
+ "triu",
301
+ "uint8",
302
+ "unique",
303
+ "zeros",
304
+ "zeros_like",
305
+ ]
torchmlx/_autograd.py ADDED
@@ -0,0 +1,135 @@
1
+ import mlx.core as mx
2
+ import mlx.nn as nn
3
+
4
+
5
+ _active_optimizer = None
6
+ _forward_depth = 0
7
+ _latest_forward = None
8
+ _loss_plans = {}
9
+ _lineage = {}
10
+ _replayed_losses = {}
11
+ _suspended = False
12
+
13
+
14
+ def _snapshot_random_state():
15
+ state = [key + mx.array(0, dtype=key.dtype) for key in mx.random.state]
16
+ mx.eval(state)
17
+ return state
18
+
19
+
20
+ def _restore_random_state(state):
21
+ for current_key, saved_key in zip(mx.random.state, state):
22
+ current_key[...] = saved_key
23
+ mx.eval(mx.random.state)
24
+
25
+
26
+ def begin_forward():
27
+ global _forward_depth
28
+ _forward_depth += 1
29
+
30
+
31
+ def end_forward(model, args, kwargs, output):
32
+ global _forward_depth, _latest_forward
33
+ _forward_depth -= 1
34
+ if (
35
+ _forward_depth == 0
36
+ and _active_optimizer is not None
37
+ and model is _active_optimizer._model
38
+ and not _suspended
39
+ and isinstance(output, mx.array)
40
+ ):
41
+ _latest_forward = (model, args, kwargs, lambda value: value)
42
+ _lineage[id(output)] = (output, _latest_forward)
43
+
44
+
45
+ def abort_forward():
46
+ global _forward_depth
47
+ _forward_depth -= 1
48
+
49
+
50
+ def activate(optimizer):
51
+ global _active_optimizer, _latest_forward
52
+ _active_optimizer = optimizer
53
+ _latest_forward = None
54
+ _loss_plans.clear()
55
+ _lineage.clear()
56
+ _replayed_losses.clear()
57
+ optimizer._random_before_forward = _snapshot_random_state()
58
+
59
+
60
+ def propagate(source, result, operation=lambda value: value):
61
+ entry = _lineage.get(id(source))
62
+ if not _suspended and entry is not None and entry[0] is source:
63
+ model, args, kwargs, previous = entry[1]
64
+ _lineage[id(result)] = (
65
+ result,
66
+ (
67
+ model,
68
+ args,
69
+ kwargs,
70
+ lambda output: operation(previous(output)),
71
+ ),
72
+ )
73
+ return result
74
+
75
+
76
+ def register_loss(loss, loss_input, rebuild):
77
+ if _suspended or _active_optimizer is None:
78
+ return loss
79
+ entry = _lineage.get(id(loss_input))
80
+ if entry is None or entry[0] is not loss_input:
81
+ return loss
82
+ model, args, kwargs, transform = entry[1]
83
+
84
+ def plan():
85
+ def objective():
86
+ return rebuild(transform(model(*args, **kwargs)))
87
+
88
+ return nn.value_and_grad(model, objective)()
89
+
90
+ _loss_plans[id(loss)] = (loss, model, plan)
91
+ return loss
92
+
93
+
94
+ def backward(loss):
95
+ global _suspended
96
+ if _active_optimizer is None:
97
+ raise RuntimeError("optimizer.zero_grad() must be called before loss.backward()")
98
+ entry = _loss_plans.get(id(loss))
99
+ if entry is None or entry[0] is not loss:
100
+ raise RuntimeError(
101
+ "this MLX loss cannot use backward compatibility; compute it with a supported torchmlx loss function"
102
+ )
103
+ _, model, plan = entry
104
+ random_after_forward = _snapshot_random_state()
105
+ _restore_random_state(_active_optimizer._random_before_forward)
106
+ _suspended = True
107
+ try:
108
+ replayed_loss, gradients = plan()
109
+ mx.eval(replayed_loss, gradients, mx.random.state)
110
+ finally:
111
+ _suspended = False
112
+ _restore_random_state(random_after_forward)
113
+ _replayed_losses[id(loss)] = (loss, replayed_loss)
114
+ _active_optimizer._pending_update = (model, gradients)
115
+
116
+
117
+ def replayed_value(value):
118
+ entry = _replayed_losses.get(id(value))
119
+ if entry is None or entry[0] is not value:
120
+ return value
121
+ return entry[1]
122
+
123
+
124
+ def step(optimizer):
125
+ global _active_optimizer, _latest_forward
126
+ if optimizer._pending_update is None:
127
+ raise RuntimeError("loss.backward() must be called before optimizer.step()")
128
+ model, gradients = optimizer._pending_update
129
+ optimizer._optimizer.update(model, gradients)
130
+ mx.eval(model.parameters(), optimizer._optimizer.state, mx.random.state)
131
+ optimizer._pending_update = None
132
+ _active_optimizer = None
133
+ _latest_forward = None
134
+ _loss_plans.clear()
135
+ _lineage.clear()
torchmlx/_backend.py ADDED
@@ -0,0 +1,27 @@
1
+ import os
2
+ import platform
3
+
4
+
5
+ def _select_backend():
6
+ override = os.environ.get("TORCHMLX_BACKEND")
7
+ if override is not None:
8
+ backend = override.lower()
9
+ if backend not in {"mlx", "torch"}:
10
+ raise RuntimeError(
11
+ "TORCHMLX_BACKEND must be either 'mlx' or 'torch', "
12
+ f"not {override!r}"
13
+ )
14
+ return backend
15
+ if platform.system() == "Darwin" and platform.machine() == "arm64":
16
+ return "mlx"
17
+ return "torch"
18
+
19
+
20
+ BACKEND = _select_backend()
21
+
22
+
23
+ def unsupported(api):
24
+ raise NotImplementedError(
25
+ f"{api} is not supported by the MLX backend. "
26
+ "Set TORCHMLX_BACKEND=torch before importing torchmlx to use PyTorch."
27
+ )
@@ -0,0 +1,240 @@
1
+ import mlx.core as mx
2
+
3
+
4
+ _transpose = mx.array.transpose
5
+ _reshape = mx.array.reshape
6
+ _squeeze = mx.array.squeeze
7
+ _mean = mx.array.mean
8
+ _var = mx.array.var
9
+ _any = mx.array.any
10
+ _getitem = mx.array.__getitem__
11
+ _setitem = mx.array.__setitem__
12
+ _item = mx.array.item
13
+
14
+
15
+ def _torch_transpose(self, dim0=None, dim1=None):
16
+ if dim1 is None:
17
+ if dim0 is None:
18
+ axes = list(reversed(range(self.ndim)))
19
+ else:
20
+ axes = dim0
21
+ else:
22
+ axes = list(range(self.ndim))
23
+ axes[dim0], axes[dim1] = axes[dim1], axes[dim0]
24
+ result = _transpose(self, axes)
25
+ from ._autograd import propagate
26
+
27
+ return propagate(self, result, lambda value: _transpose(value, axes))
28
+
29
+
30
+ def _view(self, *shape):
31
+ if len(shape) == 1 and isinstance(shape[0], (tuple, list)):
32
+ shape = shape[0]
33
+ return _torch_reshape(self, shape)
34
+
35
+
36
+ def _torch_reshape(self, *shape):
37
+ if len(shape) == 1 and isinstance(shape[0], (tuple, list)):
38
+ shape = shape[0]
39
+ result = _reshape(self, shape)
40
+ from ._autograd import propagate
41
+
42
+ return propagate(self, result, lambda value: _reshape(value, shape))
43
+
44
+
45
+ def _unsqueeze(self, dim):
46
+ result = mx.expand_dims(self, axis=dim)
47
+ from ._autograd import propagate
48
+
49
+ return propagate(self, result, lambda value: mx.expand_dims(value, axis=dim))
50
+
51
+
52
+ def _torch_squeeze(self, dim=None):
53
+ result = _squeeze(self, axis=dim)
54
+ from ._autograd import propagate
55
+
56
+ return propagate(self, result, lambda value: _squeeze(value, axis=dim))
57
+
58
+
59
+ def _flatten(self, start_dim=0, end_dim=-1):
60
+ if end_dim < 0:
61
+ end_dim += self.ndim
62
+ flattened = 1
63
+ for dimension in self.shape[start_dim : end_dim + 1]:
64
+ flattened *= dimension
65
+ shape = self.shape[:start_dim] + (flattened,) + self.shape[end_dim + 1 :]
66
+ return _torch_reshape(self, shape)
67
+
68
+
69
+ def _float(self):
70
+ return self.astype(mx.float32)
71
+
72
+
73
+ def _bool(self):
74
+ return self.astype(mx.bool_)
75
+
76
+
77
+ def _pow(self, exponent):
78
+ return mx.power(self, exponent)
79
+
80
+
81
+ def _torch_mean(self, dim=None, keepdim=False, dtype=None):
82
+ value = self.astype(dtype) if dtype is not None else self
83
+ return _mean(value, axis=dim, keepdims=keepdim)
84
+
85
+
86
+ def _torch_var(
87
+ self,
88
+ dim=None,
89
+ unbiased=True,
90
+ keepdim=False,
91
+ *,
92
+ correction=None,
93
+ ):
94
+ ddof = int(unbiased) if correction is None else correction
95
+ return _var(self, axis=dim, keepdims=keepdim, ddof=ddof)
96
+
97
+
98
+ def _size(self, dim=None):
99
+ return self.shape if dim is None else self.shape[dim]
100
+
101
+
102
+ def _chunk(self, chunks, dim=0):
103
+ if chunks <= 0:
104
+ raise ValueError("chunks must be greater than 0")
105
+ length = self.shape[dim]
106
+ if length == 0:
107
+ return tuple(mx.split(self, chunks, axis=dim))
108
+ chunk_size = (length + chunks - 1) // chunks
109
+ indices = list(range(chunk_size, length, chunk_size))
110
+ return tuple(mx.split(self, indices, axis=dim))
111
+
112
+
113
+ def _to(self, *args, dtype=None, device=None, **kwargs):
114
+ if kwargs:
115
+ name = next(iter(kwargs))
116
+ raise TypeError(f"to() got an unexpected keyword argument {name!r}")
117
+ for value in args:
118
+ if isinstance(value, mx.Dtype):
119
+ dtype = value
120
+ elif isinstance(value, mx.array):
121
+ dtype = value.dtype
122
+ else:
123
+ device = value
124
+ if device is not None and str(device) != "mps":
125
+ raise ValueError("the MLX backend only accepts device='mps'")
126
+ return self.astype(dtype) if dtype is not None and dtype != self.dtype else self
127
+
128
+
129
+ def _repeat_interleave(self, repeats, dim=None):
130
+ return mx.repeat(self, repeats, axis=dim)
131
+
132
+
133
+ def _masked_fill(self, mask, value):
134
+ return mx.where(mask, mx.array(value, dtype=self.dtype), self)
135
+
136
+
137
+ def _expand(self, *sizes):
138
+ if len(sizes) == 1 and isinstance(sizes[0], (tuple, list)):
139
+ sizes = tuple(sizes[0])
140
+ if len(sizes) < self.ndim:
141
+ raise ValueError("expanded size must have at least as many dimensions as the tensor")
142
+ source = (1,) * (len(sizes) - self.ndim) + self.shape
143
+ target = tuple(current if requested == -1 else requested for requested, current in zip(sizes, source))
144
+ return mx.broadcast_to(self.reshape(source), target)
145
+
146
+
147
+ def _torch_any(self, dim=None, keepdim=False):
148
+ return _any(self, axis=dim, keepdims=keepdim)
149
+
150
+
151
+ def _contiguous(self, memory_format=None):
152
+ return self
153
+
154
+
155
+ def _requires_grad(self, requires_grad=True):
156
+ if requires_grad:
157
+ raise RuntimeError(
158
+ "requires_grad_ is not supported by the MLX backend; use torchmlx.Trainer"
159
+ )
160
+ return self
161
+
162
+
163
+ def _backward(self, *args, **kwargs):
164
+ if args or kwargs:
165
+ raise TypeError("MLX backward compatibility does not accept arguments")
166
+ from ._autograd import backward
167
+
168
+ backward(self)
169
+
170
+
171
+ def _torch_item(self):
172
+ from ._autograd import replayed_value
173
+
174
+ return _item(replayed_value(self))
175
+
176
+
177
+ def _mask_indices(mask):
178
+ flat = mask.reshape(-1)
179
+ count = int(mx.sum(flat).item())
180
+ order = mx.argsort(flat.astype(mx.int32))
181
+ if count == 0:
182
+ return order[:0].astype(mx.int64)
183
+ return order[-count:].astype(mx.int64)
184
+
185
+
186
+ def _torch_getitem(self, key):
187
+ if isinstance(key, mx.array) and key.dtype == mx.bool_:
188
+ indices = _mask_indices(key)
189
+ if key.shape == self.shape:
190
+ result = _getitem(self.reshape(-1), indices)
191
+ operation = lambda value: _getitem(_reshape(value, (-1,)), indices)
192
+ else:
193
+ result = _getitem(self, indices)
194
+ operation = lambda value: _getitem(value, indices)
195
+ else:
196
+ result = _getitem(self, key)
197
+ operation = lambda value: _getitem(value, key)
198
+ from ._autograd import propagate
199
+
200
+ return propagate(self, result, operation)
201
+
202
+
203
+ def _torch_setitem(self, key, value):
204
+ if isinstance(key, mx.array) and key.dtype == mx.bool_:
205
+ indices = _mask_indices(key)
206
+ if key.shape == self.shape:
207
+ flat = self.reshape(-1)
208
+ _setitem(flat, indices, value)
209
+ return
210
+ _setitem(self, indices, value)
211
+ return
212
+ _setitem(self, key, value)
213
+
214
+
215
+ def install(device_type):
216
+ mx.array.transpose = _torch_transpose
217
+ mx.array.reshape = _torch_reshape
218
+ mx.array.view = _view
219
+ mx.array.unsqueeze = _unsqueeze
220
+ mx.array.squeeze = _torch_squeeze
221
+ mx.array.flatten = _flatten
222
+ mx.array.float = _float
223
+ mx.array.bool = _bool
224
+ mx.array.pow = _pow
225
+ mx.array.mean = _torch_mean
226
+ mx.array.var = _torch_var
227
+ mx.array.size = _size
228
+ mx.array.chunk = _chunk
229
+ mx.array.to = _to
230
+ mx.array.repeat_interleave = _repeat_interleave
231
+ mx.array.masked_fill = _masked_fill
232
+ mx.array.expand = _expand
233
+ mx.array.any = _torch_any
234
+ mx.array.contiguous = _contiguous
235
+ mx.array.requires_grad_ = _requires_grad
236
+ mx.array.backward = _backward
237
+ mx.array.item = _torch_item
238
+ mx.array.device = property(lambda self: device_type("mps"))
239
+ mx.array.__getitem__ = _torch_getitem
240
+ mx.array.__setitem__ = _torch_setitem
@@ -0,0 +1,257 @@
1
+ from torchmlx._backend import BACKEND, unsupported
2
+
3
+
4
+ if BACKEND == "torch":
5
+ import torch.nn as _native
6
+
7
+ Module = _native.Module
8
+ ModuleList = _native.ModuleList
9
+ Linear = _native.Linear
10
+ Embedding = _native.Embedding
11
+ LayerNorm = _native.LayerNorm
12
+ Sequential = _native.Sequential
13
+ GELU = _native.GELU
14
+ Dropout = _native.Dropout
15
+ Parameter = _native.Parameter
16
+
17
+ def __getattr__(name):
18
+ return getattr(_native, name)
19
+
20
+ else:
21
+ from collections.abc import Iterable, MutableSequence
22
+
23
+ import mlx.core as mx
24
+ import mlx.nn as _native
25
+
26
+ class _ParameterTree(dict):
27
+ def __init__(self, values, model):
28
+ super().__init__(values)
29
+ self.model = model
30
+
31
+ class Module(_native.Module):
32
+ def __call__(self, *args, **kwargs):
33
+ from torchmlx._autograd import abort_forward, begin_forward, end_forward
34
+
35
+ begin_forward()
36
+ try:
37
+ output = self.forward(*args, **kwargs)
38
+ except Exception:
39
+ abort_forward()
40
+ raise
41
+ end_forward(self, args, kwargs, output)
42
+ return output
43
+
44
+ def forward(self, *args, **kwargs):
45
+ raise NotImplementedError(
46
+ f"Module [{type(self).__name__}] is missing the required forward function"
47
+ )
48
+
49
+ def parameters(self):
50
+ return _ParameterTree(super().parameters(), self)
51
+
52
+ def to(self, *args, **kwargs):
53
+ dtype = kwargs.pop("dtype", None)
54
+ device = kwargs.pop("device", None)
55
+ if kwargs:
56
+ name = next(iter(kwargs))
57
+ raise TypeError(f"to() got an unexpected keyword argument {name!r}")
58
+ for value in args:
59
+ if isinstance(value, mx.Dtype):
60
+ if dtype is not None:
61
+ raise TypeError("to() received dtype more than once")
62
+ dtype = value
63
+ else:
64
+ if device is not None:
65
+ raise TypeError("to() received device more than once")
66
+ device = value
67
+ if device is not None and str(device) != "mps":
68
+ raise ValueError("the MLX backend only accepts device='mps'")
69
+ if dtype is not None:
70
+ self.set_dtype(dtype)
71
+ return self
72
+
73
+ def register_buffer(self, name, tensor, persistent=True):
74
+ if not isinstance(name, str) or "." in name or name == "":
75
+ raise KeyError("buffer name must be a non-empty string without dots")
76
+ setattr(self, name, tensor)
77
+ self.freeze(recurse=False, keys=name, strict=True)
78
+
79
+ def Parameter(data=None, requires_grad=True):
80
+ if data is None:
81
+ return mx.array([])
82
+ if not requires_grad:
83
+ unsupported("torchmlx.nn.Parameter with requires_grad=False")
84
+ return data
85
+
86
+ class Linear(Module, _native.Linear):
87
+ def __init__(self, in_features, out_features, bias=True, device=None, dtype=None):
88
+ Module.__init__(self)
89
+ if device is not None and str(device) != "mps":
90
+ raise ValueError("the MLX backend only accepts device='mps'")
91
+ scale = (1 / in_features) ** 0.5
92
+ self.weight = mx.random.uniform(
93
+ low=-scale, high=scale, shape=(out_features, in_features)
94
+ )
95
+ if bias:
96
+ self.bias = mx.random.uniform(
97
+ low=-scale, high=scale, shape=(out_features,)
98
+ )
99
+ if dtype is not None:
100
+ self.set_dtype(dtype)
101
+
102
+ def forward(self, input):
103
+ return _native.Linear.__call__(self, input)
104
+
105
+ class Embedding(Module, _native.Embedding):
106
+ def __init__(
107
+ self,
108
+ num_embeddings,
109
+ embedding_dim,
110
+ padding_idx=None,
111
+ max_norm=None,
112
+ norm_type=2.0,
113
+ scale_grad_by_freq=False,
114
+ sparse=False,
115
+ device=None,
116
+ dtype=None,
117
+ ):
118
+ if any(
119
+ value is not None
120
+ for value in (padding_idx, max_norm)
121
+ ) or norm_type != 2.0 or scale_grad_by_freq or sparse:
122
+ unsupported("torchmlx.nn.Embedding with non-default options")
123
+ Module.__init__(self)
124
+ if device is not None and str(device) != "mps":
125
+ raise ValueError("the MLX backend only accepts device='mps'")
126
+ scale = (1 / embedding_dim) ** 0.5
127
+ self.weight = mx.random.normal(
128
+ shape=(num_embeddings, embedding_dim), scale=scale
129
+ )
130
+ if dtype is not None:
131
+ self.set_dtype(dtype)
132
+
133
+ def forward(self, input):
134
+ return self.weight[input]
135
+
136
+ class LayerNorm(Module, _native.LayerNorm):
137
+ def __init__(
138
+ self,
139
+ normalized_shape,
140
+ eps=1e-5,
141
+ elementwise_affine=True,
142
+ bias=True,
143
+ device=None,
144
+ dtype=None,
145
+ ):
146
+ if isinstance(normalized_shape, Iterable) and not isinstance(
147
+ normalized_shape, (str, bytes)
148
+ ):
149
+ shape = tuple(normalized_shape)
150
+ if len(shape) != 1:
151
+ unsupported("torchmlx.nn.LayerNorm with multidimensional shape")
152
+ normalized_shape = shape[0]
153
+ if device is not None and str(device) != "mps":
154
+ raise ValueError("the MLX backend only accepts device='mps'")
155
+ _native.LayerNorm.__init__(
156
+ self,
157
+ normalized_shape,
158
+ eps=eps,
159
+ affine=elementwise_affine,
160
+ bias=bias,
161
+ )
162
+ if dtype is not None:
163
+ self.set_dtype(dtype)
164
+
165
+ def forward(self, input):
166
+ return _native.LayerNorm.__call__(self, input)
167
+
168
+ class Sequential(Module):
169
+ def __init__(self, *args):
170
+ super().__init__()
171
+ self.layers = list(args)
172
+
173
+ def forward(self, input):
174
+ for layer in self.layers:
175
+ input = layer(input)
176
+ return input
177
+
178
+ def __len__(self):
179
+ return len(self.layers)
180
+
181
+ def __getitem__(self, index):
182
+ if isinstance(index, str):
183
+ return dict.__getitem__(self, index)
184
+ return self.layers[index]
185
+
186
+ class GELU(Module, _native.GELU):
187
+ def __init__(self, approximate="none"):
188
+ if approximate not in {"none", "tanh"}:
189
+ raise ValueError("approximate must be 'none' or 'tanh'")
190
+ _native.GELU.__init__(self, approx=approximate)
191
+
192
+ def forward(self, input):
193
+ return _native.GELU.__call__(self, input)
194
+
195
+ class Dropout(Module, _native.Dropout):
196
+ def __init__(self, p=0.5, inplace=False):
197
+ if inplace:
198
+ unsupported("torchmlx.nn.Dropout with inplace=True")
199
+ _native.Dropout.__init__(self, p=p)
200
+
201
+ def forward(self, input):
202
+ return _native.Dropout.__call__(self, input)
203
+
204
+ class ModuleList(Module, MutableSequence):
205
+ def __init__(self, modules=None):
206
+ super().__init__()
207
+ self.layers = list(modules or [])
208
+
209
+ def __getitem__(self, index):
210
+ if isinstance(index, str):
211
+ return dict.__getitem__(self, index)
212
+ return self.layers[index]
213
+
214
+ def __setitem__(self, index, module):
215
+ if isinstance(index, str):
216
+ dict.__setitem__(self, index, module)
217
+ return
218
+ self.layers[index] = module
219
+
220
+ def __delitem__(self, index):
221
+ if isinstance(index, str):
222
+ dict.__delitem__(self, index)
223
+ return
224
+ del self.layers[index]
225
+
226
+ def __len__(self):
227
+ return len(self.layers)
228
+
229
+ def __iter__(self):
230
+ return iter(self.layers)
231
+
232
+ def insert(self, index, module):
233
+ self.layers.insert(index, module)
234
+
235
+ def forward(self, *args, **kwargs):
236
+ unsupported("torchmlx.nn.ModuleList.forward")
237
+
238
+ def __getattr__(name):
239
+ unsupported(f"torchmlx.nn.{name}")
240
+
241
+
242
+ import importlib as _importlib
243
+
244
+ functional = _importlib.import_module("torchmlx.nn.functional")
245
+
246
+ __all__ = [
247
+ "Embedding",
248
+ "Dropout",
249
+ "GELU",
250
+ "LayerNorm",
251
+ "Linear",
252
+ "Module",
253
+ "ModuleList",
254
+ "Parameter",
255
+ "Sequential",
256
+ "functional",
257
+ ]
@@ -0,0 +1,129 @@
1
+ import math
2
+
3
+ from torchmlx._backend import BACKEND, unsupported
4
+
5
+
6
+ if BACKEND == "torch":
7
+ import torch.nn.functional as _native
8
+
9
+ cross_entropy = _native.cross_entropy
10
+ scaled_dot_product_attention = _native.scaled_dot_product_attention
11
+ silu = _native.silu
12
+ softmax = _native.softmax
13
+
14
+ def __getattr__(name):
15
+ return getattr(_native, name)
16
+
17
+ else:
18
+ import mlx.core as mx
19
+
20
+ def cross_entropy(
21
+ input,
22
+ target,
23
+ weight=None,
24
+ size_average=None,
25
+ ignore_index=-100,
26
+ reduce=None,
27
+ reduction="mean",
28
+ label_smoothing=0.0,
29
+ ):
30
+ original_input = input
31
+
32
+ def finish(loss):
33
+ from torchmlx._autograd import register_loss
34
+
35
+ def rebuild(recomputed_input):
36
+ return cross_entropy(
37
+ recomputed_input,
38
+ target,
39
+ weight=weight,
40
+ size_average=size_average,
41
+ ignore_index=ignore_index,
42
+ reduce=reduce,
43
+ reduction=reduction,
44
+ label_smoothing=label_smoothing,
45
+ )
46
+
47
+ return register_loss(loss, original_input, rebuild)
48
+
49
+ if size_average is not None or reduce is not None:
50
+ unsupported("torchmlx.nn.functional.cross_entropy legacy reductions")
51
+ if input.ndim < 2:
52
+ raise ValueError("cross_entropy input must have at least 2 dimensions")
53
+ if input.ndim == 2:
54
+ logits = input
55
+ targets = target.reshape(-1)
56
+ output_shape = target.shape
57
+ else:
58
+ axes = [0] + list(range(2, input.ndim)) + [1]
59
+ logits = input.transpose(axes).reshape(-1, input.shape[1])
60
+ targets = target.reshape(-1)
61
+ output_shape = target.shape
62
+ valid = targets != ignore_index
63
+ safe_targets = mx.where(valid, targets, mx.zeros_like(targets))
64
+ log_probabilities = logits - mx.logsumexp(logits, axis=-1, keepdims=True)
65
+ target_losses = -mx.take_along_axis(
66
+ log_probabilities, safe_targets[:, None], axis=-1
67
+ ).squeeze(-1)
68
+ if weight is None:
69
+ smooth_losses = -mx.mean(log_probabilities, axis=-1)
70
+ else:
71
+ target_losses = target_losses * weight[safe_targets]
72
+ smooth_losses = -mx.sum(
73
+ log_probabilities * weight[None, :], axis=-1
74
+ ) / logits.shape[-1]
75
+ losses = (1 - label_smoothing) * target_losses + label_smoothing * smooth_losses
76
+ losses = mx.where(valid, losses, mx.zeros_like(losses))
77
+ if reduction == "none":
78
+ return finish(losses.reshape(output_shape))
79
+ if reduction == "sum":
80
+ return finish(mx.sum(losses))
81
+ if reduction == "mean":
82
+ if weight is None:
83
+ denominator = mx.sum(valid)
84
+ else:
85
+ denominator = mx.sum(mx.where(valid, weight[safe_targets], 0))
86
+ return finish(mx.sum(losses) / denominator)
87
+ raise ValueError(f"invalid reduction {reduction!r}")
88
+
89
+ def scaled_dot_product_attention(
90
+ query,
91
+ key,
92
+ value,
93
+ attn_mask=None,
94
+ dropout_p=0.0,
95
+ is_causal=False,
96
+ scale=None,
97
+ enable_gqa=False,
98
+ ):
99
+ if dropout_p != 0.0:
100
+ unsupported(
101
+ "torchmlx.nn.functional.scaled_dot_product_attention with dropout"
102
+ )
103
+ if enable_gqa:
104
+ unsupported(
105
+ "torchmlx.nn.functional.scaled_dot_product_attention with enable_gqa"
106
+ )
107
+ if is_causal and attn_mask is not None:
108
+ raise ValueError("attn_mask cannot be set when is_causal=True")
109
+ if scale is None:
110
+ scale = 1 / math.sqrt(query.shape[-1])
111
+ mask = "causal" if is_causal else attn_mask
112
+ return mx.fast.scaled_dot_product_attention(
113
+ query, key, value, scale=scale, mask=mask
114
+ )
115
+
116
+ def silu(input, inplace=False):
117
+ if inplace:
118
+ unsupported("torchmlx.nn.functional.silu with inplace=True")
119
+ return input * mx.sigmoid(input)
120
+
121
+ def softmax(input, dim=None, dtype=None):
122
+ value = input.astype(dtype) if dtype is not None else input
123
+ return mx.softmax(value, axis=dim)
124
+
125
+ def __getattr__(name):
126
+ unsupported(f"torchmlx.nn.functional.{name}")
127
+
128
+
129
+ __all__ = ["cross_entropy", "scaled_dot_product_attention", "silu", "softmax"]
@@ -0,0 +1,72 @@
1
+ from torchmlx._backend import BACKEND, unsupported
2
+
3
+
4
+ if BACKEND == "torch":
5
+ import torch.optim as _native
6
+
7
+ AdamW = _native.AdamW
8
+
9
+ def __getattr__(name):
10
+ return getattr(_native, name)
11
+
12
+ else:
13
+ import mlx.optimizers as _native
14
+
15
+ class AdamW:
16
+ def __init__(
17
+ self,
18
+ params,
19
+ lr=1e-3,
20
+ betas=(0.9, 0.999),
21
+ eps=1e-8,
22
+ weight_decay=1e-2,
23
+ amsgrad=False,
24
+ maximize=False,
25
+ foreach=None,
26
+ capturable=False,
27
+ differentiable=False,
28
+ fused=None,
29
+ ):
30
+ if amsgrad or maximize or foreach is not None or capturable or differentiable or fused is not None:
31
+ unsupported("torchmlx.optim.AdamW with non-default options")
32
+ model = getattr(params, "model", None)
33
+ if model is None:
34
+ raise TypeError(
35
+ "MLX AdamW requires parameters returned directly by model.parameters()"
36
+ )
37
+ self._parameters = params
38
+ self._model = model
39
+ self._optimizer = _native.AdamW(
40
+ learning_rate=lr,
41
+ betas=list(betas),
42
+ eps=eps,
43
+ weight_decay=weight_decay,
44
+ bias_correction=True,
45
+ )
46
+ self._pending_update = None
47
+ self._random_before_forward = None
48
+
49
+ @property
50
+ def state(self):
51
+ return self._optimizer.state
52
+
53
+ def zero_grad(self, *args, **kwargs):
54
+ if args or kwargs:
55
+ unsupported("torchmlx.optim.AdamW.zero_grad with arguments")
56
+ from torchmlx._autograd import activate
57
+
58
+ self._pending_update = None
59
+ activate(self)
60
+
61
+ def step(self, *args, **kwargs):
62
+ if args or kwargs:
63
+ unsupported("torchmlx.optim.AdamW.step with arguments")
64
+ from torchmlx._autograd import step
65
+
66
+ step(self)
67
+
68
+ def __getattr__(name):
69
+ unsupported(f"torchmlx.optim.{name}")
70
+
71
+
72
+ __all__ = ["AdamW"]
torchmlx/trainer.py ADDED
@@ -0,0 +1,60 @@
1
+ from ._backend import BACKEND
2
+
3
+
4
+ if BACKEND == "torch":
5
+ import torch
6
+
7
+ class Trainer:
8
+ def __init__(self, model, optimizer, loss_fn, compile=True):
9
+ self.model = model
10
+ self.optimizer = optimizer
11
+ self.loss_fn = loss_fn
12
+ self._step = torch.compile(self._train_step) if compile else self._train_step
13
+
14
+ def _train_step(self, x, y):
15
+ self.optimizer.zero_grad()
16
+ loss = self.loss_fn(self.model(x), y)
17
+ loss.backward()
18
+ self.optimizer.step()
19
+ return loss
20
+
21
+ def step(self, x, y):
22
+ return self._step(x, y)
23
+
24
+ else:
25
+ import mlx.core as mx
26
+ import mlx.nn as nn
27
+
28
+ class Trainer:
29
+ def __init__(self, model, optimizer, loss_fn, compile=True):
30
+ self.model = model
31
+ self.optimizer = optimizer
32
+ self.loss_fn = loss_fn
33
+ native_optimizer = optimizer._optimizer
34
+ native_optimizer.init(model.trainable_parameters())
35
+
36
+ def objective(x, y):
37
+ return loss_fn(model(x), y)
38
+
39
+ value_and_grad = nn.value_and_grad(model, objective)
40
+
41
+ def train_step(x, y):
42
+ loss, gradients = value_and_grad(x, y)
43
+ native_optimizer.update(model, gradients)
44
+ return loss
45
+
46
+ if compile:
47
+ state = [model.state, native_optimizer.state, mx.random.state]
48
+ self._step = mx.compile(train_step, inputs=state, outputs=state)
49
+ else:
50
+ self._step = train_step
51
+
52
+ def step(self, x, y):
53
+ loss = self._step(x, y)
54
+ mx.eval(
55
+ loss,
56
+ self.model.parameters(),
57
+ self.optimizer._optimizer.state,
58
+ mx.random.state,
59
+ )
60
+ return loss