pytorchmlx 0.0.1__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.
- pytorchmlx-0.0.1/.github/workflows/publish.yml +110 -0
- pytorchmlx-0.0.1/.gitignore +14 -0
- pytorchmlx-0.0.1/.python-version +1 -0
- pytorchmlx-0.0.1/LICENSE +21 -0
- pytorchmlx-0.0.1/PKG-INFO +48 -0
- pytorchmlx-0.0.1/README.md +15 -0
- pytorchmlx-0.0.1/docs/compatibility.md +25 -0
- pytorchmlx-0.0.1/examples/tinystories-llm/train.py +263 -0
- pytorchmlx-0.0.1/pyproject.toml +22 -0
- pytorchmlx-0.0.1/src/torchmlx/__init__.py +305 -0
- pytorchmlx-0.0.1/src/torchmlx/_autograd.py +135 -0
- pytorchmlx-0.0.1/src/torchmlx/_backend.py +27 -0
- pytorchmlx-0.0.1/src/torchmlx/_mlx_tensor.py +240 -0
- pytorchmlx-0.0.1/src/torchmlx/nn/__init__.py +257 -0
- pytorchmlx-0.0.1/src/torchmlx/nn/functional.py +129 -0
- pytorchmlx-0.0.1/src/torchmlx/optim/__init__.py +72 -0
- pytorchmlx-0.0.1/src/torchmlx/trainer.py +60 -0
- pytorchmlx-0.0.1/uv.lock +803 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: publish to pypi
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
concurrency:
|
|
7
|
+
group: publish-to-pypi
|
|
8
|
+
cancel-in-progress: false
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v6
|
|
23
|
+
|
|
24
|
+
- name: select release
|
|
25
|
+
id: release
|
|
26
|
+
env:
|
|
27
|
+
GH_TOKEN: ${{ github.token }}
|
|
28
|
+
run: |
|
|
29
|
+
latest_tag=$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n 1)
|
|
30
|
+
create_tag=false
|
|
31
|
+
publish=true
|
|
32
|
+
|
|
33
|
+
if [ -z "$latest_tag" ]; then
|
|
34
|
+
target_tag=v0.0.1
|
|
35
|
+
create_tag=true
|
|
36
|
+
elif gh release view "$latest_tag" >/dev/null 2>&1; then
|
|
37
|
+
if git diff --quiet "$latest_tag"..HEAD; then
|
|
38
|
+
publish=false
|
|
39
|
+
target_tag="$latest_tag"
|
|
40
|
+
else
|
|
41
|
+
version=${latest_tag#v}
|
|
42
|
+
IFS=. read -r major minor patch <<< "$version"
|
|
43
|
+
target_tag="v${major}.${minor}.$((patch + 1))"
|
|
44
|
+
create_tag=true
|
|
45
|
+
fi
|
|
46
|
+
else
|
|
47
|
+
target_tag="$latest_tag"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [[ ! "$target_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
51
|
+
echo "invalid release tag: $target_tag"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
echo "publish=$publish"
|
|
57
|
+
echo "create_tag=$create_tag"
|
|
58
|
+
echo "tag=$target_tag"
|
|
59
|
+
echo "version=${target_tag#v}"
|
|
60
|
+
} >> "$GITHUB_OUTPUT"
|
|
61
|
+
|
|
62
|
+
- name: stop when unchanged
|
|
63
|
+
if: steps.release.outputs.publish == 'false'
|
|
64
|
+
run: echo "no changes since ${{ steps.release.outputs.tag }}"
|
|
65
|
+
|
|
66
|
+
- name: verify pypi credentials
|
|
67
|
+
if: steps.release.outputs.publish == 'true'
|
|
68
|
+
env:
|
|
69
|
+
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
70
|
+
run: |
|
|
71
|
+
if [ -z "$PYPI_API_TOKEN" ]; then
|
|
72
|
+
echo "PYPI_API_TOKEN is not configured"
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
- name: set package version
|
|
77
|
+
if: steps.release.outputs.publish == 'true'
|
|
78
|
+
env:
|
|
79
|
+
VERSION: ${{ steps.release.outputs.version }}
|
|
80
|
+
run: sed -i "0,/version = \".*\"/s//version = \"$VERSION\"/" pyproject.toml
|
|
81
|
+
|
|
82
|
+
- name: build package
|
|
83
|
+
if: steps.release.outputs.publish == 'true'
|
|
84
|
+
run: uv build
|
|
85
|
+
|
|
86
|
+
- name: check package
|
|
87
|
+
if: steps.release.outputs.publish == 'true'
|
|
88
|
+
run: uv tool run twine check dist/*
|
|
89
|
+
|
|
90
|
+
- name: create tag
|
|
91
|
+
if: steps.release.outputs.publish == 'true' && steps.release.outputs.create_tag == 'true'
|
|
92
|
+
env:
|
|
93
|
+
TAG: ${{ steps.release.outputs.tag }}
|
|
94
|
+
run: |
|
|
95
|
+
git tag "$TAG" "$GITHUB_SHA"
|
|
96
|
+
git push origin "$TAG"
|
|
97
|
+
|
|
98
|
+
- name: publish package
|
|
99
|
+
if: steps.release.outputs.publish == 'true'
|
|
100
|
+
env:
|
|
101
|
+
TWINE_USERNAME: __token__
|
|
102
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
103
|
+
run: uv tool run twine upload --verbose dist/*
|
|
104
|
+
|
|
105
|
+
- name: create github release
|
|
106
|
+
if: steps.release.outputs.publish == 'true'
|
|
107
|
+
env:
|
|
108
|
+
GH_TOKEN: ${{ github.token }}
|
|
109
|
+
TAG: ${{ steps.release.outputs.tag }}
|
|
110
|
+
run: gh release create "$TAG" dist/* --generate-notes --title "$TAG"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
pytorchmlx-0.0.1/LICENSE
ADDED
|
@@ -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.
|
|
@@ -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,15 @@
|
|
|
1
|
+
# torchmlx
|
|
2
|
+
|
|
3
|
+
torchmlx is a pytorch-shaped compatibility layer that uses mlx on apple silicon and pytorch elsewhere.
|
|
4
|
+
|
|
5
|
+
NOTE: it is experimental and built first for educational use. If you find a bug, please create an issue on the repo.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import torchmlx as torch
|
|
9
|
+
from torchmlx import nn, optim
|
|
10
|
+
|
|
11
|
+
model = nn.Linear(4, 2)
|
|
12
|
+
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
see the [tinystories example](examples/tinystories-llm/train.py) and [compatibility details](docs/compatibility.md).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
TorchMLX targets the common transformer operations used by GPT-2, Llama 3, Qwen 3, and GPT-OSS style implementations.
|
|
4
|
+
|
|
5
|
+
Supported MLX operations include embeddings, linear layers, normalization building blocks, dropout, activations, causal attention, tensor shape operations, masks, top-k routing, and AdamW training.
|
|
6
|
+
|
|
7
|
+
MLX arrays remain native arrays. Torch-style tensor methods are installed on the native array type for the supported subset.
|
|
8
|
+
|
|
9
|
+
Boolean expert routing and `unique` execute eagerly because their output shapes control Python flow.
|
|
10
|
+
|
|
11
|
+
The standard training sequence works with `cross_entropy` losses applied directly or after reshape, view, slicing, transpose, squeeze, unsqueeze, or flatten operations:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
optimizer.zero_grad()
|
|
15
|
+
logits = model(input_tokens)
|
|
16
|
+
loss = F.cross_entropy(logits.reshape(-1, vocabulary_size), targets.reshape(-1))
|
|
17
|
+
loss.backward()
|
|
18
|
+
optimizer.step()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
MLX implements this sequence by recording the outer model call and replaying it inside `value_and_grad` during `backward`. Random state is restored for the replay so dropout uses the same mask. Unrecorded loss expressions, gradient hooks, parameter `.grad`, higher-order gradients, and multiple-forward losses remain unsupported.
|
|
22
|
+
|
|
23
|
+
Set `TORCHMLX_BACKEND=torch` before import to use native PyTorch for unsupported programs. TorchMLX never changes backend during an operation.
|
|
24
|
+
|
|
25
|
+
The referenced OpenArch model files contain source errors independent of TorchMLX, including invalid constructor calls and undefined attributes. Correct those errors before using either backend.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import urllib.request
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import torchmlx as torch
|
|
7
|
+
from torchmlx import nn, optim
|
|
8
|
+
import torchmlx.nn.functional as F
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
device = torch.device(
|
|
12
|
+
"mps"
|
|
13
|
+
if torch.backends.mps.is_available()
|
|
14
|
+
else "cuda"
|
|
15
|
+
if torch.cuda.is_available()
|
|
16
|
+
else "cpu"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DATA_URL = "https://huggingface.co/datasets/roneneldan/TinyStories/resolve/main/TinyStoriesV2-GPT4-valid.txt"
|
|
20
|
+
DATA_PATH = Path(__file__).with_name("TinyStoriesV2-GPT4-valid.txt")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TransformerConfig:
|
|
25
|
+
vocabulary_size: int
|
|
26
|
+
context_length: int = 128
|
|
27
|
+
model_dimension: int = 32
|
|
28
|
+
head_count: int = 16
|
|
29
|
+
layer_count: int = 4
|
|
30
|
+
feed_forward_dimension: int = 128
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TrainingConfig:
|
|
35
|
+
batch_size: int = 32
|
|
36
|
+
step_count: int = 5000
|
|
37
|
+
learning_rate: float = 1e-3
|
|
38
|
+
weight_decay: float = 1e-2
|
|
39
|
+
log_interval: int = 100
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CharacterTokenizer:
|
|
43
|
+
def __init__(self, text):
|
|
44
|
+
self.characters = sorted(set(text))
|
|
45
|
+
self.token_by_character = {
|
|
46
|
+
character: token for token, character in enumerate(self.characters)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def encode(self, text):
|
|
50
|
+
return [self.token_by_character[character] for character in text]
|
|
51
|
+
|
|
52
|
+
def decode(self, tokens):
|
|
53
|
+
return "".join(self.characters[int(token)] for token in tokens)
|
|
54
|
+
|
|
55
|
+
def __len__(self):
|
|
56
|
+
return len(self.characters)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TokenEmbedding(nn.Module):
|
|
60
|
+
def __init__(self, config):
|
|
61
|
+
super().__init__()
|
|
62
|
+
self.embedding = nn.Embedding(
|
|
63
|
+
config.vocabulary_size, config.model_dimension
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def forward(self, tokens):
|
|
67
|
+
return self.embedding(tokens)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PositionalEmbedding(nn.Module):
|
|
71
|
+
def __init__(self, config):
|
|
72
|
+
super().__init__()
|
|
73
|
+
self.embedding = nn.Embedding(
|
|
74
|
+
config.context_length, config.model_dimension
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def forward(self, tokens):
|
|
78
|
+
positions = torch.arange(tokens.shape[1], device=tokens.device)
|
|
79
|
+
return self.embedding(positions)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CausalSelfAttention(nn.Module):
|
|
83
|
+
def __init__(self, config):
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.head_count = config.head_count
|
|
86
|
+
self.head_dimension = config.model_dimension // config.head_count
|
|
87
|
+
self.query = nn.Linear(config.model_dimension, config.model_dimension)
|
|
88
|
+
self.key = nn.Linear(config.model_dimension, config.model_dimension)
|
|
89
|
+
self.value = nn.Linear(config.model_dimension, config.model_dimension)
|
|
90
|
+
self.output = nn.Linear(config.model_dimension, config.model_dimension)
|
|
91
|
+
|
|
92
|
+
def split_heads(self, hidden_states):
|
|
93
|
+
batch_size, sequence_length, _ = hidden_states.shape
|
|
94
|
+
hidden_states = hidden_states.reshape(
|
|
95
|
+
batch_size, sequence_length, self.head_count, self.head_dimension
|
|
96
|
+
)
|
|
97
|
+
return hidden_states.transpose(1, 2)
|
|
98
|
+
|
|
99
|
+
def merge_heads(self, hidden_states):
|
|
100
|
+
batch_size, _, sequence_length, _ = hidden_states.shape
|
|
101
|
+
hidden_states = hidden_states.transpose(1, 2).contiguous()
|
|
102
|
+
return hidden_states.reshape(
|
|
103
|
+
batch_size, sequence_length, self.head_count * self.head_dimension
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def forward(self, hidden_states):
|
|
107
|
+
queries = self.split_heads(self.query(hidden_states))
|
|
108
|
+
keys = self.split_heads(self.key(hidden_states))
|
|
109
|
+
values = self.split_heads(self.value(hidden_states))
|
|
110
|
+
|
|
111
|
+
attention_scores = torch.matmul(queries, keys.transpose(-2, -1))
|
|
112
|
+
attention_scores = attention_scores / self.head_dimension**0.5
|
|
113
|
+
|
|
114
|
+
sequence_length = hidden_states.shape[1]
|
|
115
|
+
future_positions = torch.triu(
|
|
116
|
+
torch.ones(
|
|
117
|
+
sequence_length,
|
|
118
|
+
sequence_length,
|
|
119
|
+
dtype=torch.bool,
|
|
120
|
+
device=hidden_states.device,
|
|
121
|
+
),
|
|
122
|
+
diagonal=1,
|
|
123
|
+
)
|
|
124
|
+
attention_scores = attention_scores.masked_fill(
|
|
125
|
+
future_positions, float("-inf")
|
|
126
|
+
)
|
|
127
|
+
attention_weights = F.softmax(attention_scores, dim=-1)
|
|
128
|
+
attended_values = torch.matmul(attention_weights, values)
|
|
129
|
+
return self.output(self.merge_heads(attended_values))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class FeedForward(nn.Module):
|
|
133
|
+
def __init__(self, config):
|
|
134
|
+
super().__init__()
|
|
135
|
+
self.layers = nn.Sequential(
|
|
136
|
+
nn.Linear(config.model_dimension, config.feed_forward_dimension),
|
|
137
|
+
nn.GELU(),
|
|
138
|
+
nn.Linear(config.feed_forward_dimension, config.model_dimension),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def forward(self, hidden_states):
|
|
142
|
+
return self.layers(hidden_states)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Unembedding(nn.Module):
|
|
146
|
+
def __init__(self, config):
|
|
147
|
+
super().__init__()
|
|
148
|
+
self.output = nn.Linear(
|
|
149
|
+
config.model_dimension, config.vocabulary_size, bias=False
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def forward(self, hidden_states):
|
|
153
|
+
return self.output(hidden_states)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TransformerBlock(nn.Module):
|
|
157
|
+
def __init__(self, config):
|
|
158
|
+
super().__init__()
|
|
159
|
+
self.attention_norm = nn.LayerNorm(config.model_dimension)
|
|
160
|
+
self.attention = CausalSelfAttention(config)
|
|
161
|
+
self.feed_forward_norm = nn.LayerNorm(config.model_dimension)
|
|
162
|
+
self.feed_forward = FeedForward(config)
|
|
163
|
+
|
|
164
|
+
def forward(self, hidden_states):
|
|
165
|
+
hidden_states = hidden_states + self.attention(
|
|
166
|
+
self.attention_norm(hidden_states)
|
|
167
|
+
)
|
|
168
|
+
return hidden_states + self.feed_forward(
|
|
169
|
+
self.feed_forward_norm(hidden_states)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TinyStoriesTransformer(nn.Module):
|
|
174
|
+
def __init__(self, config):
|
|
175
|
+
super().__init__()
|
|
176
|
+
self.config = config
|
|
177
|
+
self.token_embedding = TokenEmbedding(config)
|
|
178
|
+
self.position_embedding = PositionalEmbedding(config)
|
|
179
|
+
self.blocks = nn.ModuleList(
|
|
180
|
+
[TransformerBlock(config) for _ in range(config.layer_count)]
|
|
181
|
+
)
|
|
182
|
+
self.final_norm = nn.LayerNorm(config.model_dimension)
|
|
183
|
+
self.unembedding = Unembedding(config)
|
|
184
|
+
|
|
185
|
+
def forward(self, tokens):
|
|
186
|
+
hidden_states = self.token_embedding(tokens)
|
|
187
|
+
hidden_states = hidden_states + self.position_embedding(tokens)
|
|
188
|
+
for block in self.blocks:
|
|
189
|
+
hidden_states = block(hidden_states)
|
|
190
|
+
return self.unembedding(self.final_norm(hidden_states))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def load_tiny_stories():
|
|
194
|
+
if not DATA_PATH.exists():
|
|
195
|
+
print(f"downloading TinyStories to {DATA_PATH}")
|
|
196
|
+
urllib.request.urlretrieve(DATA_URL, DATA_PATH)
|
|
197
|
+
return DATA_PATH.read_text(encoding="utf-8")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def create_batch(encoded_text, batch_size, context_length):
|
|
201
|
+
starts = [
|
|
202
|
+
random.randrange(len(encoded_text) - context_length - 1)
|
|
203
|
+
for _ in range(batch_size)
|
|
204
|
+
]
|
|
205
|
+
input_tokens = [
|
|
206
|
+
encoded_text[start : start + context_length] for start in starts
|
|
207
|
+
]
|
|
208
|
+
target_tokens = [
|
|
209
|
+
encoded_text[start + 1 : start + context_length + 1]
|
|
210
|
+
for start in starts
|
|
211
|
+
]
|
|
212
|
+
return torch.tensor(input_tokens, dtype=torch.long, device=device), torch.tensor(
|
|
213
|
+
target_tokens, dtype=torch.long, device=device
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def language_model_loss(logits, target_tokens):
|
|
218
|
+
vocabulary_size = logits.shape[-1]
|
|
219
|
+
return F.cross_entropy(
|
|
220
|
+
logits.reshape(-1, vocabulary_size), target_tokens.reshape(-1)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def generate_text(model, tokenizer, prompt, token_count):
|
|
225
|
+
generated_tokens = tokenizer.encode(prompt)
|
|
226
|
+
model.eval()
|
|
227
|
+
for _ in range(token_count):
|
|
228
|
+
context = generated_tokens[-model.config.context_length :]
|
|
229
|
+
input_tokens = torch.tensor([context], dtype=torch.long, device=device)
|
|
230
|
+
next_token_logits = model(input_tokens)[:, -1, :]
|
|
231
|
+
next_token = torch.categorical(next_token_logits).item()
|
|
232
|
+
generated_tokens.append(next_token)
|
|
233
|
+
return tokenizer.decode(generated_tokens)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
training_text = load_tiny_stories()
|
|
237
|
+
tokenizer = CharacterTokenizer(training_text)
|
|
238
|
+
config = TransformerConfig(vocabulary_size=len(tokenizer))
|
|
239
|
+
training_config = TrainingConfig()
|
|
240
|
+
encoded_text = tokenizer.encode(training_text)
|
|
241
|
+
model = TinyStoriesTransformer(config).to(device)
|
|
242
|
+
optimizer = optim.AdamW(
|
|
243
|
+
model.parameters(),
|
|
244
|
+
lr=training_config.learning_rate,
|
|
245
|
+
weight_decay=training_config.weight_decay,
|
|
246
|
+
)
|
|
247
|
+
model.train()
|
|
248
|
+
|
|
249
|
+
for step in range(training_config.step_count):
|
|
250
|
+
input_tokens, target_tokens = create_batch(
|
|
251
|
+
encoded_text,
|
|
252
|
+
batch_size=training_config.batch_size,
|
|
253
|
+
context_length=config.context_length,
|
|
254
|
+
)
|
|
255
|
+
optimizer.zero_grad()
|
|
256
|
+
logits = model(input_tokens)
|
|
257
|
+
loss = language_model_loss(logits, target_tokens)
|
|
258
|
+
loss.backward()
|
|
259
|
+
optimizer.step()
|
|
260
|
+
if step % training_config.log_interval == 0:
|
|
261
|
+
print(f"step {step}: loss {loss.item():.4f}")
|
|
262
|
+
|
|
263
|
+
print(generate_text(model, tokenizer, "Once upon a time", token_count=120))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pytorchmlx"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "An educational PyTorch-shaped interface for MLX and PyTorch"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Priyanshu Jain" },
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mlx>=0.32.2,<0.33",
|
|
13
|
+
"numpy",
|
|
14
|
+
"torch>=2.4,<3",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/torchmlx"]
|