torch-ane
Run PyTorch models on the Apple Neural Engine through Apple's Core AI (macOS 27+).
Pass in a plain nn.Module, or a model from transformers or diffusers. torch-ane rewrites it
into a form the ANE runs fast and correctly: fp16, static shapes, and the layout and ops from
Apple's ANE guidance. It also works around the ANE's numeric quirks. You get back a module
that behaves like the original.
import torch_ane
m = torch_ane.compile(model.eval(), example_inputs=(x,)) # any nn.Module -> ANE
y = m(x) # torch tensors in, torch tensors out
On an M4 MacBook, ANE prefill and convolutions are 2–3× faster than the GPU (MLX/MPS). Decoding is limited by memory bandwidth, and the ANE reads ~57 GB/s.
Install
Requires macOS 27+ on Apple silicon, and Python 3.11+.
pip install torch-ane # core: compile()
pip install "torch-ane[llm]" # + CausalLM / ChunkedCausalLM (transformers, gguf)
pip install "torch-ane[diffusion]" # + StableDiffusion (diffusers)
Core AI's Python packages (coreai-core, coreai-torch, coreai-opt) come from PyPI
as dependencies.
What's inside
import torch_ane
torch_ane.available() # is Core AI usable here
# Any module. The first call per input shape builds a program (seconds); then it's cached on disk.
m = torch_ane.compile(model, example_inputs=(x,), device="ane") # or device="gpu"
# A whole LLM on the ANE (up to ~1-3B). The KV cache is Core AI state, so it is never copied.
lm = torch_ane.CausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", batch=True)
ids = lm.generate(prompt_ids, max_new_tokens=64, eos=tok.eos_token_id)
outs = lm.generate_batch([ids1, ids2, ids3], max_new_tokens=64) # up to 8 streams per step
logits = lm.feed(more_ids) # append to the cache -> last logits
# A large model from GGUF, in 4-layer chunks with 4-bit palettes (Devstral 24B etc.)
big = torch_ane.ChunkedCausalLM.from_gguf("model.gguf", "config_dir/")
# Stable Diffusion: text encoder, UNet and VAE on the ANE
sd = torch_ane.StableDiffusion("segmind/tiny-sd")
image, timings = sd("a cute robot", steps=20, seed=0)
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 |
CompiledModule |
torch module over Core AI programs, one program per input shape (up to 8) |
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 |
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 |
StableDiffusion(model_id, device="ane", size=512) |
diffusers pipeline with all three parts on the ANE |
passes |
the building blocks: export_for_ane, swap_linear, split_wide, chunk_mlp, palettize_lut, quantize_int8_modules, calibrate_rescale, use_composite_ops, … |
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 |
What it does to your model
These passes need no knowledge of the specific model:
- RMSNorm, found by a numerical check rather than by name, becomes a Core AI composite op.
- Everything that doesn't depend on the input (masks, RoPE tables) is precomputed in fp32.
- A causal mask becomes
is_causal. - SiLU becomes
x·0.5·(1 + tanh(x/2)). The native ANE SiLU is off by ~5% near zero, which wrecks GroupNorm→SiLU in diffusion UNets. - The model is converted to fp16, the only float type the ANE runs.
- MLPs whose projection weight exceeds 4 MB are split along the hidden dimension. Each chunk
of
upfeeds its own chunk ofdown, and the results are summed. Otherwise the weight plus the[S, hidden]activation doesn't fit the ANE's on-chip memory (~32 MB on M4), and the ANE stalls on the bus at ~50 GB/s. This cuts the Whisper large encoder by 24% and Qwen2.5-0.5B prefill by 14%. Disable it withrules=().
LLMs get two more passes:
- Per-projection 2^k scales, calibrated on real text, so small activations don't become fp16 subnormals while large ones don't overflow.
- int16 positions, embeddings on the CPU, and logits for one row only.
UNet/VAE attention uses Apple's layout: BC1S, heads split, einsum with no transposes.
Speed (MacBook M4, 24 GB)
| task | ANE | for comparison |
|---|---|---|
| ResNet-18, batch 16 | 4.5 ms | MLX 24.5, MPS 32.7 |
| BERT-base 8×128 | 20 ms | MLX 71, MPS 75 |
| Whisper large-v3-turbo encoder | 285 ms (int8: 264) | |
| Qwen2.5-0.5B prefill, 512 tokens | 9065 tok/s | mlx-lm 3279 |
| Qwen2.5-0.5B decode, fp16 | 54 tok/s; 8 streams: 351 | mlx-lm bf16 70 |
| Devstral Small 24B decode | 4.8 tok/s | mlx-lm 4-bit 6.7 |
| tiny-sd 512×512, 20 steps | 4.9 s | VAE 3× faster than GPU |
Examples
examples/compile_bert.py: BERT-base, eager vs ANEexamples/generate.py: chat with Qwen2.5 on the ANEexamples/chat_gguf.py: chat with a 24B GGUF model, with the engine in a restartable processexamples/imagine.py: Stable Diffusion
Limitations
- Shapes are static, so a new input shape means a new build.
- The ANE computes in fp16 only (weights can be palettized or int8).
-infin masks is replaced by −40000. - int8 weights in Conv1d make the ANE compiler fail silently, and the whole function moves to
the GPU.
compress="int8"therefore quantizes only Linear/Conv2d. - The coreai-core Python wrapper aborts the process after 16 384 allocated buffers
(apple/coreai-torch#75).
ChunkedCausalLM.steps_left()lets you restart ahead of time; seeexamples/chat_gguf.py. - The ANE unloads programs after ~10 s idle, which makes the next call ~3× slower. Use
ChunkedCausalLM.keep_warm()to avoid this. - Copying an input into Core AI runs at ~0.5 GB/s, about 10 ms per 4 MB input. The public API has no way around this.
- Code comments are in Russian.
Tests
pip install -e ".[llm,diffusion,dev]"
pytest
License
MIT
Metadata
Release files for torch-ane 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| torch_ane-0.1.0.tar.gz | 54.0 kB | Without provenance | Details for torch_ane-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| torch_ane-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for torch_ane-0.1.0-py3-none-any.whl |
Total release size: 110.9 kB
Release files / torch_ane-0.1.0.tar.gz
| Download URL | torch_ane-0.1.0.tar.gz |
|---|---|
| Size | 54.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3063612f431e5dfc67109108f2cbb9f13d2aac1b4cdd4d9f11b3d544ff0ef13b
|
|
BLAKE2b-256 checksum How to use checksums |
c8bbdeb958e3683ad35f2b4388432b99a1cdf349315a41147f2559cb5e69fe30
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Release files / torch_ane-0.1.0-py3-none-any.whl
| Download URL | torch_ane-0.1.0-py3-none-any.whl |
|---|---|
| Size | 56.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2a8f535e1f1b743177be215a6ca97ba003cba1e7ed6ae3ddf48dd62955a1169b
|
|
BLAKE2b-256 checksum How to use checksums |
1e795bb8e18dd060dfba35fcacab8a25e9bc688fc3eba99c7c40c8db74ab0ded
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Security for torch-ane 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.