torch-ane 0.1.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Shivalov
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,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: torch-ane
3
+ Version: 0.1.0
4
+ Summary: Run PyTorch models on the Apple Neural Engine via Core AI: compile any nn.Module, LLMs with KV cache, Stable Diffusion
5
+ Author: Ivan Shivalov
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/intexcor/torch-ane
8
+ Project-URL: Issues, https://github.com/intexcor/torch-ane/issues
9
+ Keywords: pytorch,apple-silicon,neural-engine,ane,core-ai,coreml,llm,stable-diffusion
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: torch>=2.10
20
+ Requires-Dist: numpy
21
+ Requires-Dist: coreai-torch>=0.4.3
22
+ Requires-Dist: coreai-opt>=0.3
23
+ Provides-Extra: llm
24
+ Requires-Dist: transformers>=5; extra == "llm"
25
+ Requires-Dist: gguf; extra == "llm"
26
+ Provides-Extra: diffusion
27
+ Requires-Dist: diffusers>=0.40; extra == "diffusion"
28
+ Requires-Dist: transformers>=5; extra == "diffusion"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: pyflakes; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # torch-ane
35
+
36
+ Run PyTorch models on the **Apple Neural Engine** through Apple's Core AI (macOS 27+).
37
+
38
+ Pass in a plain `nn.Module`, or a model from transformers or diffusers. torch-ane rewrites it
39
+ into a form the ANE runs fast and correctly: fp16, static shapes, and the layout and ops from
40
+ Apple's ANE guidance. It also works around the ANE's numeric quirks. You get back a module
41
+ that behaves like the original.
42
+
43
+ ```python
44
+ import torch_ane
45
+
46
+ m = torch_ane.compile(model.eval(), example_inputs=(x,)) # any nn.Module -> ANE
47
+ y = m(x) # torch tensors in, torch tensors out
48
+ ```
49
+
50
+ On an M4 MacBook, ANE prefill and convolutions are 2–3× faster than the GPU (MLX/MPS).
51
+ Decoding is limited by memory bandwidth, and the ANE reads ~57 GB/s.
52
+
53
+ ## Install
54
+
55
+ Requires macOS 27+ on Apple silicon, and Python 3.11+.
56
+
57
+ ```sh
58
+ pip install torch-ane # core: compile()
59
+ pip install "torch-ane[llm]" # + CausalLM / ChunkedCausalLM (transformers, gguf)
60
+ pip install "torch-ane[diffusion]" # + StableDiffusion (diffusers)
61
+ ```
62
+
63
+ Core AI's Python packages (`coreai-core`, `coreai-torch`, `coreai-opt`) come from PyPI
64
+ as dependencies.
65
+
66
+ ## What's inside
67
+
68
+ ```python
69
+ import torch_ane
70
+
71
+ torch_ane.available() # is Core AI usable here
72
+
73
+ # Any module. The first call per input shape builds a program (seconds); then it's cached on disk.
74
+ m = torch_ane.compile(model, example_inputs=(x,), device="ane") # or device="gpu"
75
+
76
+ # A whole LLM on the ANE (up to ~1-3B). The KV cache is Core AI state, so it is never copied.
77
+ lm = torch_ane.CausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", batch=True)
78
+ ids = lm.generate(prompt_ids, max_new_tokens=64, eos=tok.eos_token_id)
79
+ outs = lm.generate_batch([ids1, ids2, ids3], max_new_tokens=64) # up to 8 streams per step
80
+ logits = lm.feed(more_ids) # append to the cache -> last logits
81
+
82
+ # A large model from GGUF, in 4-layer chunks with 4-bit palettes (Devstral 24B etc.)
83
+ big = torch_ane.ChunkedCausalLM.from_gguf("model.gguf", "config_dir/")
84
+
85
+ # Stable Diffusion: text encoder, UNet and VAE on the ANE
86
+ sd = torch_ane.StableDiffusion("segmind/tiny-sd")
87
+ image, timings = sd("a cute robot", steps=20, seed=0)
88
+ ```
89
+
90
+ | | |
91
+ |---|---|
92
+ | `compile(model, example_inputs=None, device="ane", validate=True, compress=None)` | → `CompiledModule`. `validate` checks the first call of each shape against eager. If they disagree, that shape falls back to GPU, then to eager. `compress="int8"` stores Linear/Conv2d weights in int8 |
93
+ | `CompiledModule` | torch module over Core AI programs, one program per input shape (up to 8) |
94
+ | `CausalLM(hf_model, max_len=2048, compress=None, batch=False, ...)` | prefill in chunks of 256, decode in windows of 8, a ladder of context sizes 512…`max_len` in one program |
95
+ | `ChunkedCausalLM.from_gguf(path, config)` | 4-bit palettes, plus int8 where the GGUF is more precise (Q6_K). First build takes ~35 min for 24B |
96
+ | `StableDiffusion(model_id, device="ane", size=512)` | diffusers pipeline with all three parts on the ANE |
97
+ | `passes` | the building blocks: `export_for_ane`, `swap_linear`, `split_wide`, `chunk_mlp`, `palettize_lut`, `quantize_int8_modules`, `calibrate_rescale`, `use_composite_ops`, … |
98
+ | `CACHE_DIR`, `prune_cache()`, `clear_system_cache()` | build cache at `~/.cache/torch_ane` (`TORCH_ANE_CACHE`). It is capped at `TORCH_ANE_CACHE_GB` (40) and least recently used builds are removed. `clear_system_cache()` wipes Core AI's own compile cache, which can only be cleared as a whole |
99
+
100
+ ## What it does to your model
101
+
102
+ These passes need no knowledge of the specific model:
103
+
104
+ 1. RMSNorm, found by a numerical check rather than by name, becomes a Core AI composite op.
105
+ 2. Everything that doesn't depend on the input (masks, RoPE tables) is precomputed in fp32.
106
+ 3. A causal mask becomes `is_causal`.
107
+ 4. SiLU becomes `x·0.5·(1 + tanh(x/2))`. The native ANE SiLU is off by ~5% near zero, which
108
+ wrecks GroupNorm→SiLU in diffusion UNets.
109
+ 5. The model is converted to fp16, the only float type the ANE runs.
110
+ 6. MLPs whose projection weight exceeds 4 MB are split along the hidden dimension. Each chunk
111
+ of `up` feeds its own chunk of `down`, and the results are summed. Otherwise the weight plus
112
+ the `[S, hidden]` activation doesn't fit the ANE's on-chip memory (~32 MB on M4), and the
113
+ ANE stalls on the bus at ~50 GB/s. This cuts the Whisper large encoder by 24% and
114
+ Qwen2.5-0.5B prefill by 14%. Disable it with `rules=()`.
115
+
116
+ LLMs get two more passes:
117
+ - Per-projection 2^k scales, calibrated on real text, so small activations don't become fp16
118
+ subnormals while large ones don't overflow.
119
+ - int16 positions, embeddings on the CPU, and logits for one row only.
120
+
121
+ UNet/VAE attention uses Apple's layout: BC1S, heads split, `einsum` with no transposes.
122
+
123
+ ## Speed (MacBook M4, 24 GB)
124
+
125
+ | task | ANE | for comparison |
126
+ |---|---|---|
127
+ | ResNet-18, batch 16 | 4.5 ms | MLX 24.5, MPS 32.7 |
128
+ | BERT-base 8×128 | 20 ms | MLX 71, MPS 75 |
129
+ | Whisper large-v3-turbo encoder | 285 ms (int8: 264) | |
130
+ | Qwen2.5-0.5B prefill, 512 tokens | 9065 tok/s | mlx-lm 3279 |
131
+ | Qwen2.5-0.5B decode, fp16 | 54 tok/s; 8 streams: 351 | mlx-lm bf16 70 |
132
+ | Devstral Small 24B decode | 4.8 tok/s | mlx-lm 4-bit 6.7 |
133
+ | tiny-sd 512×512, 20 steps | 4.9 s | VAE 3× faster than GPU |
134
+
135
+ ## Examples
136
+
137
+ - [`examples/compile_bert.py`](examples/compile_bert.py): BERT-base, eager vs ANE
138
+ - [`examples/generate.py`](examples/generate.py): chat with Qwen2.5 on the ANE
139
+ - [`examples/chat_gguf.py`](examples/chat_gguf.py): chat with a 24B GGUF model, with the engine in a restartable process
140
+ - [`examples/imagine.py`](examples/imagine.py): Stable Diffusion
141
+
142
+ ## Limitations
143
+
144
+ - Shapes are static, so a new input shape means a new build.
145
+ - The ANE computes in fp16 only (weights can be palettized or int8). `-inf` in masks is
146
+ replaced by −40000.
147
+ - int8 weights in Conv1d make the ANE compiler fail silently, and the whole function moves to
148
+ the GPU. `compress="int8"` therefore quantizes only Linear/Conv2d.
149
+ - The coreai-core Python wrapper aborts the process after 16 384 allocated buffers
150
+ ([apple/coreai-torch#75](https://github.com/apple/coreai-torch/issues/75)).
151
+ `ChunkedCausalLM.steps_left()` lets you restart ahead of time; see `examples/chat_gguf.py`.
152
+ - The ANE unloads programs after ~10 s idle, which makes the next call ~3× slower. Use
153
+ `ChunkedCausalLM.keep_warm()` to avoid this.
154
+ - Copying an input into Core AI runs at ~0.5 GB/s, about 10 ms per 4 MB input. The public API
155
+ has no way around this.
156
+ - Code comments are in Russian.
157
+
158
+ ## Tests
159
+
160
+ ```sh
161
+ pip install -e ".[llm,diffusion,dev]"
162
+ pytest
163
+ ```
164
+
165
+ ## License
166
+
167
+ MIT
@@ -0,0 +1,134 @@
1
+ # torch-ane
2
+
3
+ Run PyTorch models on the **Apple Neural Engine** through Apple's Core AI (macOS 27+).
4
+
5
+ Pass in a plain `nn.Module`, or a model from transformers or diffusers. torch-ane rewrites it
6
+ into a form the ANE runs fast and correctly: fp16, static shapes, and the layout and ops from
7
+ Apple's ANE guidance. It also works around the ANE's numeric quirks. You get back a module
8
+ that behaves like the original.
9
+
10
+ ```python
11
+ import torch_ane
12
+
13
+ m = torch_ane.compile(model.eval(), example_inputs=(x,)) # any nn.Module -> ANE
14
+ y = m(x) # torch tensors in, torch tensors out
15
+ ```
16
+
17
+ On an M4 MacBook, ANE prefill and convolutions are 2–3× faster than the GPU (MLX/MPS).
18
+ Decoding is limited by memory bandwidth, and the ANE reads ~57 GB/s.
19
+
20
+ ## Install
21
+
22
+ Requires macOS 27+ on Apple silicon, and Python 3.11+.
23
+
24
+ ```sh
25
+ pip install torch-ane # core: compile()
26
+ pip install "torch-ane[llm]" # + CausalLM / ChunkedCausalLM (transformers, gguf)
27
+ pip install "torch-ane[diffusion]" # + StableDiffusion (diffusers)
28
+ ```
29
+
30
+ Core AI's Python packages (`coreai-core`, `coreai-torch`, `coreai-opt`) come from PyPI
31
+ as dependencies.
32
+
33
+ ## What's inside
34
+
35
+ ```python
36
+ import torch_ane
37
+
38
+ torch_ane.available() # is Core AI usable here
39
+
40
+ # Any module. The first call per input shape builds a program (seconds); then it's cached on disk.
41
+ m = torch_ane.compile(model, example_inputs=(x,), device="ane") # or device="gpu"
42
+
43
+ # A whole LLM on the ANE (up to ~1-3B). The KV cache is Core AI state, so it is never copied.
44
+ lm = torch_ane.CausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", batch=True)
45
+ ids = lm.generate(prompt_ids, max_new_tokens=64, eos=tok.eos_token_id)
46
+ outs = lm.generate_batch([ids1, ids2, ids3], max_new_tokens=64) # up to 8 streams per step
47
+ logits = lm.feed(more_ids) # append to the cache -> last logits
48
+
49
+ # A large model from GGUF, in 4-layer chunks with 4-bit palettes (Devstral 24B etc.)
50
+ big = torch_ane.ChunkedCausalLM.from_gguf("model.gguf", "config_dir/")
51
+
52
+ # Stable Diffusion: text encoder, UNet and VAE on the ANE
53
+ sd = torch_ane.StableDiffusion("segmind/tiny-sd")
54
+ image, timings = sd("a cute robot", steps=20, seed=0)
55
+ ```
56
+
57
+ | | |
58
+ |---|---|
59
+ | `compile(model, example_inputs=None, device="ane", validate=True, compress=None)` | → `CompiledModule`. `validate` checks the first call of each shape against eager. If they disagree, that shape falls back to GPU, then to eager. `compress="int8"` stores Linear/Conv2d weights in int8 |
60
+ | `CompiledModule` | torch module over Core AI programs, one program per input shape (up to 8) |
61
+ | `CausalLM(hf_model, max_len=2048, compress=None, batch=False, ...)` | prefill in chunks of 256, decode in windows of 8, a ladder of context sizes 512…`max_len` in one program |
62
+ | `ChunkedCausalLM.from_gguf(path, config)` | 4-bit palettes, plus int8 where the GGUF is more precise (Q6_K). First build takes ~35 min for 24B |
63
+ | `StableDiffusion(model_id, device="ane", size=512)` | diffusers pipeline with all three parts on the ANE |
64
+ | `passes` | the building blocks: `export_for_ane`, `swap_linear`, `split_wide`, `chunk_mlp`, `palettize_lut`, `quantize_int8_modules`, `calibrate_rescale`, `use_composite_ops`, … |
65
+ | `CACHE_DIR`, `prune_cache()`, `clear_system_cache()` | build cache at `~/.cache/torch_ane` (`TORCH_ANE_CACHE`). It is capped at `TORCH_ANE_CACHE_GB` (40) and least recently used builds are removed. `clear_system_cache()` wipes Core AI's own compile cache, which can only be cleared as a whole |
66
+
67
+ ## What it does to your model
68
+
69
+ These passes need no knowledge of the specific model:
70
+
71
+ 1. RMSNorm, found by a numerical check rather than by name, becomes a Core AI composite op.
72
+ 2. Everything that doesn't depend on the input (masks, RoPE tables) is precomputed in fp32.
73
+ 3. A causal mask becomes `is_causal`.
74
+ 4. SiLU becomes `x·0.5·(1 + tanh(x/2))`. The native ANE SiLU is off by ~5% near zero, which
75
+ wrecks GroupNorm→SiLU in diffusion UNets.
76
+ 5. The model is converted to fp16, the only float type the ANE runs.
77
+ 6. MLPs whose projection weight exceeds 4 MB are split along the hidden dimension. Each chunk
78
+ of `up` feeds its own chunk of `down`, and the results are summed. Otherwise the weight plus
79
+ the `[S, hidden]` activation doesn't fit the ANE's on-chip memory (~32 MB on M4), and the
80
+ ANE stalls on the bus at ~50 GB/s. This cuts the Whisper large encoder by 24% and
81
+ Qwen2.5-0.5B prefill by 14%. Disable it with `rules=()`.
82
+
83
+ LLMs get two more passes:
84
+ - Per-projection 2^k scales, calibrated on real text, so small activations don't become fp16
85
+ subnormals while large ones don't overflow.
86
+ - int16 positions, embeddings on the CPU, and logits for one row only.
87
+
88
+ UNet/VAE attention uses Apple's layout: BC1S, heads split, `einsum` with no transposes.
89
+
90
+ ## Speed (MacBook M4, 24 GB)
91
+
92
+ | task | ANE | for comparison |
93
+ |---|---|---|
94
+ | ResNet-18, batch 16 | 4.5 ms | MLX 24.5, MPS 32.7 |
95
+ | BERT-base 8×128 | 20 ms | MLX 71, MPS 75 |
96
+ | Whisper large-v3-turbo encoder | 285 ms (int8: 264) | |
97
+ | Qwen2.5-0.5B prefill, 512 tokens | 9065 tok/s | mlx-lm 3279 |
98
+ | Qwen2.5-0.5B decode, fp16 | 54 tok/s; 8 streams: 351 | mlx-lm bf16 70 |
99
+ | Devstral Small 24B decode | 4.8 tok/s | mlx-lm 4-bit 6.7 |
100
+ | tiny-sd 512×512, 20 steps | 4.9 s | VAE 3× faster than GPU |
101
+
102
+ ## Examples
103
+
104
+ - [`examples/compile_bert.py`](examples/compile_bert.py): BERT-base, eager vs ANE
105
+ - [`examples/generate.py`](examples/generate.py): chat with Qwen2.5 on the ANE
106
+ - [`examples/chat_gguf.py`](examples/chat_gguf.py): chat with a 24B GGUF model, with the engine in a restartable process
107
+ - [`examples/imagine.py`](examples/imagine.py): Stable Diffusion
108
+
109
+ ## Limitations
110
+
111
+ - Shapes are static, so a new input shape means a new build.
112
+ - The ANE computes in fp16 only (weights can be palettized or int8). `-inf` in masks is
113
+ replaced by −40000.
114
+ - int8 weights in Conv1d make the ANE compiler fail silently, and the whole function moves to
115
+ the GPU. `compress="int8"` therefore quantizes only Linear/Conv2d.
116
+ - The coreai-core Python wrapper aborts the process after 16 384 allocated buffers
117
+ ([apple/coreai-torch#75](https://github.com/apple/coreai-torch/issues/75)).
118
+ `ChunkedCausalLM.steps_left()` lets you restart ahead of time; see `examples/chat_gguf.py`.
119
+ - The ANE unloads programs after ~10 s idle, which makes the next call ~3× slower. Use
120
+ `ChunkedCausalLM.keep_warm()` to avoid this.
121
+ - Copying an input into Core AI runs at ~0.5 GB/s, about 10 ms per 4 MB input. The public API
122
+ has no way around this.
123
+ - Code comments are in Russian.
124
+
125
+ ## Tests
126
+
127
+ ```sh
128
+ pip install -e ".[llm,diffusion,dev]"
129
+ pytest
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "torch-ane"
7
+ version = "0.1.0"
8
+ description = "Run PyTorch models on the Apple Neural Engine via Core AI: compile any nn.Module, LLMs with KV cache, Stable Diffusion"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Ivan Shivalov" }]
13
+ requires-python = ">=3.11"
14
+ dependencies = ["torch>=2.10", "numpy", "coreai-torch>=0.4.3", "coreai-opt>=0.3"]
15
+ keywords = ["pytorch", "apple-silicon", "neural-engine", "ane", "core-ai", "coreml", "llm", "stable-diffusion"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Operating System :: MacOS",
19
+ "Programming Language :: Python :: 3",
20
+ "Intended Audience :: Developers",
21
+ "Intended Audience :: Science/Research",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ llm = ["transformers>=5", "gguf"]
27
+ diffusion = ["diffusers>=0.40", "transformers>=5"]
28
+ dev = ["pytest", "pyflakes"]
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/intexcor/torch-ane"
32
+ Issues = "https://github.com/intexcor/torch-ane/issues"
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["torch_ane*"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,207 @@
1
+ """torch_ane: компиляция на Neural Engine, проходы, LLM с KV-кешем-состоянием."""
2
+ import subprocess
3
+ import sys
4
+
5
+ import pytest
6
+ import torch
7
+ import torch.nn as nn
8
+
9
+ import torch_ane
10
+
11
+ needs_coreai = pytest.mark.skipif(not torch_ane.available(), reason="нет Core AI (macOS 27 + coreai-torch)")
12
+
13
+
14
+ def test_no_mlx_dependency():
15
+ """Библиотека самостоятельна: импорт torch_ane и его LLM-части не тянет MLX и torch_mlx."""
16
+ code = ("import sys, torch_ane; torch_ane.CausalLM; torch_ane.passes.swap_linear; "
17
+ "print(any(m == 'mlx' or m.startswith(('mlx.', 'torch_mlx')) for m in sys.modules))")
18
+ out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True).stdout
19
+ assert out.strip() == "False"
20
+
21
+
22
+ def test_prune_cache(tmp_path, monkeypatch):
23
+ """Кеш сборок не больше лимита: удаляются давно не использованные, keep и
24
+ недописанные (.partial) остаются."""
25
+ import os
26
+
27
+ from torch_ane import runtime
28
+ monkeypatch.setattr(runtime, "CACHE_DIR", tmp_path)
29
+ for i, name in enumerate(("a.aimodel", "b.aimodel", "c.aimodel", "d.partial.aimodel")):
30
+ d = tmp_path / name
31
+ d.mkdir()
32
+ (d / "w").write_bytes(b"x" * 1000)
33
+ os.utime(d, (1000 + i, 1000 + i))
34
+ freed = runtime.prune_cache(max_gb=2.5e-6, keep=(tmp_path / "a.aimodel",)) # лимит 2500 байт
35
+ assert freed == 2000 and sorted(p.name for p in tmp_path.iterdir()) == ["a.aimodel", "d.partial.aimodel"]
36
+
37
+
38
+ def test_device_names():
39
+ m = nn.Linear(4, 4)
40
+ if torch_ane.available():
41
+ assert torch_ane.CompiledModule(m, device="ane").device == "neural_engine"
42
+ assert torch_ane.CompiledModule(m, device="gpu").device == "gpu"
43
+ with pytest.raises(ValueError):
44
+ torch_ane.runtime._device("tpu")
45
+
46
+
47
+ @needs_coreai
48
+ def test_compile_matches_eager():
49
+ torch.manual_seed(0)
50
+ m = nn.Sequential(nn.Linear(32, 64), nn.GELU(), nn.Linear(64, 8)).eval()
51
+ x = torch.randn(4, 32)
52
+ cm = torch_ane.compile(m, example_inputs=(x,))
53
+ with torch.no_grad():
54
+ ref = m(x)
55
+ out = cm(x)
56
+ assert out.dtype == torch.float32 and ((out - ref).norm() / ref.norm()) < 2e-2
57
+
58
+
59
+ @needs_coreai
60
+ def test_compile_int8_with_conv1d():
61
+ """compress="int8" на ANE: int8 только в Linear/Conv2d. Сжатие готовой программы
62
+ квантовало и Conv1d — компилятор ANE падал, модель уходила на GPU с мусором на выходе."""
63
+ class Net(nn.Module):
64
+ def __init__(self):
65
+ super().__init__()
66
+ self.conv = nn.Conv1d(16, 64, 3, padding=1)
67
+ self.mlp = nn.Sequential(nn.Linear(64, 256), nn.GELU(), nn.Linear(256, 64))
68
+
69
+ def forward(self, x): # [1, 16, T]
70
+ h = torch.nn.functional.gelu(self.conv(x)).transpose(1, 2)
71
+ return h + self.mlp(h)
72
+
73
+ torch.manual_seed(0)
74
+ m = Net().eval()
75
+ x = torch.randn(1, 16, 128)
76
+ cm = torch_ane.compile(m, example_inputs=(x,), validate=False, compress="int8")
77
+ with torch.no_grad():
78
+ ref = m(x)
79
+ out = cm(x)
80
+ if cm.ane_stats:
81
+ assert cm.ane_stats["int8"] == 2 # две Linear, Conv1d не тронута
82
+ assert ((out - ref).norm() / ref.norm()) < 3e-2
83
+
84
+
85
+ @needs_coreai
86
+ def test_ane_passes_llm():
87
+ """ANE-проходы на LLM из transformers без правок под модель: RMSNorm -> составная операция,
88
+ свёртка констант (маска, RoPE), причинная маска -> is_causal; следующий токен как у eager."""
89
+ from transformers import Qwen2Config, Qwen2ForCausalLM
90
+
91
+ class Prefill(nn.Module):
92
+ def __init__(self, lm):
93
+ super().__init__()
94
+ self.lm = lm
95
+
96
+ def forward(self, ids):
97
+ return self.lm.lm_head(self.lm.model(input_ids=ids, use_cache=False).last_hidden_state[:, -1])
98
+
99
+ torch.manual_seed(0)
100
+ lm = Qwen2ForCausalLM(Qwen2Config(hidden_size=128, intermediate_size=256, num_hidden_layers=2,
101
+ num_attention_heads=4, num_key_value_heads=2, vocab_size=1000)).eval()
102
+ model = Prefill(lm).eval()
103
+ ids = torch.randint(0, 1000, (1, 64))
104
+ m = torch_ane.compile(model, validate=False)
105
+ out = m(ids)
106
+ with torch.no_grad():
107
+ ref = model(ids)
108
+ if m.ane_stats: # при загрузке из дискового кеша проходы не запускаются
109
+ assert {k: m.ane_stats[k] for k in ("rms_norm", "causal")} == {"rms_norm": 5, "causal": 2}
110
+ assert m.ane_stats["folded"] > 0
111
+ assert int(out.argmax()) == int(ref.argmax())
112
+ assert ((out.float() - ref).norm() / ref.norm()) < 5e-2
113
+
114
+
115
+ def test_ane_rules_eager_equivalent():
116
+ """Правила Apple для ANE (Linear -> свёртка 1x1, внимание по головам) не меняют
117
+ математику модели: проверка в eager, без Core AI."""
118
+ from transformers import Qwen2Config, Qwen2ForCausalLM
119
+
120
+ from torch_ane import passes
121
+ torch.manual_seed(0)
122
+ lm = Qwen2ForCausalLM(Qwen2Config(hidden_size=64, intermediate_size=128, num_hidden_layers=2,
123
+ num_attention_heads=4, num_key_value_heads=2, vocab_size=100)).eval()
124
+ ids = torch.randint(0, 100, (1, 23))
125
+ with torch.no_grad():
126
+ ref = lm(ids).logits
127
+ for rules in (("conv",), ("conv_n111",), ("heads",), ("conv", "heads")):
128
+ m, st = passes.prepare(lm, rules)
129
+ assert st.get("conv", 0) or st.get("heads", 0)
130
+ torch.testing.assert_close(m(ids).logits, ref, rtol=1e-4, atol=1e-4)
131
+
132
+
133
+ def test_chunk_mlp_eager_equivalent():
134
+ """MLP кусками скрытого измерения: Sequential (Whisper) и gate/up/down (Qwen) — та же
135
+ математика; маленькие MLP не трогаются."""
136
+ from transformers import Qwen2Config, Qwen2ForCausalLM
137
+
138
+ from torch_ane import passes
139
+ torch.manual_seed(0)
140
+ seq = nn.Sequential(nn.Sequential(nn.Linear(256, 1024), nn.GELU(), nn.Linear(1024, 256))).eval()
141
+ lm = Qwen2ForCausalLM(Qwen2Config(hidden_size=64, intermediate_size=256, num_hidden_layers=2,
142
+ num_attention_heads=4, num_key_value_heads=2, vocab_size=100)).eval()
143
+ x, ids = torch.randn(3, 256), torch.randint(0, 100, (1, 11))
144
+ with torch.no_grad():
145
+ ref_s, ref_l = seq(x), lm(ids).logits
146
+ m, st = passes.prepare(seq, ("mlp_chunk",))
147
+ assert st["mlp_chunk"] == 0 # вес 0.5 МБ — меньше порога
148
+ assert passes.chunk_mlp(m, max_bytes=128 << 10) == 1 and len(m[0].up) == 4
149
+ torch.testing.assert_close(m(x), ref_s, rtol=1e-4, atol=1e-5)
150
+ m, _ = passes.prepare(lm, ())
151
+ assert passes.chunk_mlp(m, max_bytes=8 << 10) == 2 and len(m.model.layers[0].mlp.up) == 4
152
+ torch.testing.assert_close(m(ids).logits, ref_l, rtol=1e-4, atol=1e-4)
153
+ m, _ = passes.prepare(lm, ()) # down_proj в обёртке масштаба (CausalLM)
154
+ passes.rescale_for_fp16(m, k=4)
155
+ assert passes.chunk_mlp(m, max_bytes=8 << 10) == 2 and m.model.layers[0].mlp.scale == 2 ** -4
156
+ torch.testing.assert_close(m(ids).logits, ref_l, rtol=1e-4, atol=1e-4)
157
+
158
+
159
+ @needs_coreai
160
+ def test_causal_lm_matches_eager():
161
+ """KV-кеш — состояния Core AI (схема Apple): prefill куском + декод окнами по 8
162
+ дают те же логиты, что transformers на всём контексте."""
163
+ from transformers import Qwen2Config, Qwen2ForCausalLM
164
+
165
+ import numpy as np
166
+
167
+ torch.manual_seed(0)
168
+ lm = Qwen2ForCausalLM(Qwen2Config(hidden_size=128, intermediate_size=256, num_hidden_layers=2,
169
+ num_attention_heads=4, num_key_value_heads=2, vocab_size=1000)).eval()
170
+ ids = torch.randint(0, 1000, (100,)).tolist() # 37 + 63: переход со ступени 64 на 128
171
+ st = torch_ane.CausalLM(lm, max_len=256, prefill_q=32, ladder=(64, 128))
172
+ st.reset()
173
+ st._hist = []
174
+ got = [st.feed(ids[:37])] + [st.feed([t]) for t in ids[37:]]
175
+ with torch.no_grad():
176
+ ref = lm(torch.tensor([ids])).logits[0, 36:].numpy()
177
+ assert st.M == 128
178
+ for g, r in zip(got, ref):
179
+ assert np.linalg.norm(g - r) / np.linalg.norm(r) < 0.05
180
+
181
+
182
+ @needs_coreai
183
+ def test_causal_lm_batch_matches_single():
184
+ """Потоки в окне декода (generate_batch): у каждого свои позиции и маска, K/V вперемешку
185
+ в общем буфере — токены совпадают с одиночной генерацией каждого промпта."""
186
+ from transformers import Qwen2Config, Qwen2ForCausalLM
187
+
188
+ torch.manual_seed(0)
189
+ lm = Qwen2ForCausalLM(Qwen2Config(hidden_size=128, intermediate_size=256, num_hidden_layers=2,
190
+ num_attention_heads=4, num_key_value_heads=2, vocab_size=1000)).eval()
191
+ prompts = [torch.randint(0, 1000, (n,)).tolist() for n in (5, 40, 17)] # 40 — два куска prefill
192
+ st = torch_ane.CausalLM(lm, max_len=256, prefill_q=32, ladder=(128,), batch=True)
193
+ assert st.generate_batch(prompts, 12) == [st.generate(p, 12) for p in prompts]
194
+
195
+
196
+ @needs_coreai
197
+ def test_silu_after_groupnorm():
198
+ """Родная SiLU на ANE врёт около нуля (~5% после GroupNorm); export_for_ane заменяет её
199
+ записью через tanh — связка GroupNorm -> SiLU -> свёртка совпадает с eager."""
200
+ torch.manual_seed(0)
201
+ m = nn.Sequential(nn.GroupNorm(8, 64), nn.SiLU(), nn.Conv2d(64, 64, 3, padding=1)).eval()
202
+ x = torch.randn(2, 64, 16, 16) * 3
203
+ with torch.no_grad():
204
+ ref = m(x)
205
+ cm = torch_ane.compile(m, validate=False)
206
+ out = cm(x)
207
+ assert ((out - ref).norm() / ref.norm()) < 5e-3
@@ -0,0 +1,34 @@
1
+ """torch_ane — PyTorch-модели на Apple Neural Engine через Core AI (macOS 27+).
2
+
3
+ import torch_ane
4
+
5
+ m = torch_ane.compile(model.eval(), example_inputs=(x,)) # любой nn.Module -> ANE
6
+ y = m(x)
7
+
8
+ lm = torch_ane.CausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
9
+ ids = lm.generate(prompt_ids, max_new_tokens=64)
10
+
11
+ sd = torch_ane.StableDiffusion("segmind/tiny-sd")
12
+ image, timings = sd("a cute robot", steps=20, seed=0)
13
+
14
+ Нижний уровень — torch_ane.passes (проходы экспорта, раскладка Apple, палитры/int8,
15
+ составные операции). Сборки кешируются в torch_ane.CACHE_DIR (~/.cache/torch_ane,
16
+ переменная TORCH_ANE_CACHE). Библиотека не зависит от MLX.
17
+ """
18
+ from . import passes
19
+ from .runtime import (CACHE_DIR, CompiledModule, available, cleanup_mpsgraph_tmp, clear_system_cache, compile,
20
+ compress_program, model_digest, prune_cache)
21
+
22
+ __all__ = ["compile", "CompiledModule", "available", "compress_program", "model_digest", "cleanup_mpsgraph_tmp",
23
+ "CACHE_DIR", "prune_cache", "clear_system_cache", "passes", "CausalLM", "ChunkedCausalLM", "StableDiffusion"]
24
+
25
+
26
+ def __getattr__(name):
27
+ # тяжёлые части (transformers, diffusers) — только по обращению
28
+ if name in ("CausalLM", "ChunkedCausalLM"):
29
+ from . import llm
30
+ return getattr(llm, name)
31
+ if name == "StableDiffusion":
32
+ from .diffusion import StableDiffusion
33
+ return StableDiffusion
34
+ raise AttributeError(f"module 'torch_ane' has no attribute {name!r}")