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.
- pytorchmlx-0.0.1.dist-info/METADATA +48 -0
- pytorchmlx-0.0.1.dist-info/RECORD +12 -0
- pytorchmlx-0.0.1.dist-info/WHEEL +4 -0
- pytorchmlx-0.0.1.dist-info/licenses/LICENSE +21 -0
- torchmlx/__init__.py +305 -0
- torchmlx/_autograd.py +135 -0
- torchmlx/_backend.py +27 -0
- torchmlx/_mlx_tensor.py +240 -0
- torchmlx/nn/__init__.py +257 -0
- torchmlx/nn/functional.py +129 -0
- torchmlx/optim/__init__.py +72 -0
- torchmlx/trainer.py +60 -0
|
@@ -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,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
|
+
)
|
torchmlx/_mlx_tensor.py
ADDED
|
@@ -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
|
torchmlx/nn/__init__.py
ADDED
|
@@ -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
|