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.
@@ -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,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ examples/tinystories-llm/TinyStoriesV2-GPT4-valid.txt
9
+
10
+ # Virtual environments
11
+ .venv
12
+
13
+ # Environment files
14
+ .env
@@ -0,0 +1 @@
1
+ 3.13
@@ -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"]