Python bindings for Apple Metal GPU compute
Project description
mtlpy
Python bindings for GPU compute on Apple Metal, built on pybind11 and Apple's metal-cpp. Write a Metal compute kernel as a string, dispatch it over NumPy arrays, get the result back as a NumPy array — no separate build step, no manual buffer plumbing.
import numpy as np
import mtlpy
device = mtlpy.Device()
a = device.buffer(np.array([1.0, 2.0, 3.0], dtype=np.float32))
b = device.buffer(np.array([10.0, 20.0, 30.0], dtype=np.float32))
print((a + b).contents) # [11. 22. 33.]
Why this exists
mtlpy is a from-scratch rewrite of themetalgpu
project. Rather than build on top of that codebase's ctypes-based bindings
and global singleton state, mtlpy starts over with a few deliberate
improvements:
- pybind11 instead of ctypes — real type safety across the Python/C++ boundary, and Metal errors propagate as Python exceptions instead of silent failures.
- No global singleton state — each
Deviceowns its own command queue and pipeline cache; nothing is saved/restored behind your back. - Pipeline compile caching — a compute pipeline is compiled once per (shader source, function name) and reused, both within a process and (via an on-disk Metal binary archive) across process launches.
- Async dispatch —
Pipeline.run(..., wait=False)lets you batch work without stalling on every call.
Status
Alpha, but built, tested, and benchmarked on real Apple Silicon hardware — see Building from source and the test suite for current coverage.
Architecture
metal-cpp/ Apple's C++ Metal headers (git submodule)
csrc/ C++ extension (pybind11 + metal-cpp)
device.{h,cpp} MTL::Device + MTL::CommandQueue owner
buffer.{h,cpp} MTL::Buffer wrapper (shared-storage, CPU/GPU unified memory)
pipeline.{h,cpp} Dispatches a compiled MTL::ComputePipelineState
pipeline_cache.{h,cpp} Compiles-once cache, keyed on (source, function name),
backed by an on-disk MTL::BinaryArchive
metal_impl.mm Single Obj-C++ translation unit providing the
NS::/CA::/MTL:: private implementations
bindings.cpp pybind11 module definition (`_mtlpy`)
src/mtlpy/ Python package (src layout, for PyPI)
device.py Device: buffer/empty/compile, list_devices(), wraps _mtlpy.Device
buffer.py Buffer: NumPy-backed contents, arithmetic/comparison/in-place operators
pipeline.py Pipeline: thin wrapper over _mtlpy.Pipeline
operators.py sqrt/cos/sin/tan/exp/log, sum/max/min/mean reductions
shader.py Generates Metal Shading Language source per dtype
utils.py NumPy dtype <-> Metal type name mapping
tests/ pytest suite
benchmarks/ Standalone performance baseline scripts
examples/ Runnable usage examples
Each Device in Python owns exactly one MTL::Device, one MTL::CommandQueue,
and one PipelineCache. Buffers use MTL::ResourceStorageModeShared, so on
Apple Silicon's unified memory there's no copy between CPU and GPU views of
the same allocation — Buffer.contents is a NumPy array backed directly by
GPU-visible memory (accessing .contents is a true zero-copy view; writing
new data into it via buf.contents[:] = arr is still a real memcpy from
arr's own memory, same as it would be for any destination).
Features
- Elementwise operators:
+,-,*,/, unary-, and in-place+=/-=/*=//=(which dispatch in-place, into the sameBuffer, with no extra allocation) onBuffer— each also works with a NumPy/Python scalar on either side (buf + 5.0,5.0 - buf), not justBuffer op Buffer. Plussqrt,cos,sin,tan,exp,log, andastypefor dtype conversion. - Comparisons:
==,!=,<,<=,>,>=(against anotherBufferor a scalar) return aboolBuffer, matching NumPy'sndarrayconvention — which also makesBufferunhashable, same tradeoff NumPy makes. - Reductions:
operators.sum/max/min/mean— an O(log n) multi-pass tree reduction returning a plain Python scalar. - Custom kernels: compile and dispatch arbitrary Metal Shading Language
source directly (see Custom kernels below).
Pipeline.runvalidates the buffer count against the kernel's own argument reflection, so passing too few buffers raises a clear Python exception instead of leaving a Metal buffer argument unbound (undefined behavior). - Dtype support:
float32,float16,int32,uint32,int16,uint16,int64,uint64,bool— mapped to their Metal equivalents (float,half,int,uint,short,ushort,long,ulong,bool) insrc/mtlpy/utils.py.float64has no Metal equivalent (no Apple GPU supports double precision), so it's silently downcast tofloat32at buffer creation. Note thatBuffer / Bufferuses Metal's native/for the shared dtype (truncating for integers), not NumPy's always-promote-to-float64 semantics. - Pipeline caching: identical (source, function name) pairs are compiled
once per process and reused; a binary archive on disk
(
~/Library/Caches/mtlpy/pipelines.metallib) carries compiled pipelines across process launches too.Device.flush_cache()(or usingDeviceas a context manager:with mtlpy.Device() as d:) serializes it on demand, rather than only when theDeviceis garbage collected. - Async dispatch:
wait=Falsecommits work without blocking; Metal retires command buffers on a queue in commit order, so a laterwait=Truedispatch that reads the result is enough to synchronize (seeexamples/async_dispatch.py).Pipeline.runreleases the GIL for the whole call, so other Python threads keep running during the GPU wait instead of being blocked for its full duration. - Multi-GPU support:
mtlpy.list_devices()lists every Metal-capable GPU on the machine;mtlpy.Device(index=...)selects one (the default targets the system default GPU). - Errors as exceptions: shader compile failures, missing kernel
functions, mismatched buffer counts, mismatched-
Deviceoperands, and GPU execution errors all raise Python exceptions with a clear message, instead of failing silently or invoking undefined behavior.
Building from source
Requires macOS with Metal support, Xcode (for the Metal/Objective-C++ toolchain), CMake, and Python 3.9+.
git clone --recursive git@github.com:peyton-howe/mtlpy.git
cd mtlpy
pip install -e ".[dev]"
If you already cloned without --recursive:
git submodule update --init
Quick start
import numpy as np
import mtlpy
device = mtlpy.Device()
a = device.buffer(np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32))
b = device.buffer(np.array([10.0, 20.0, 30.0, 40.0], dtype=np.float32))
c = a + b
print(c.contents) # numpy.ndarray([11. 22. 33. 44.])
d = mtlpy.operators.sqrt(a)
print(d.contents)
e = a.astype(np.int32)
print(e.dtype, e.contents)
Custom kernels
Device.compile(source, function_name) compiles arbitrary Metal Shading
Language and returns a Pipeline you can dispatch directly:
source = """
#include <metal_stdlib>
using namespace metal;
kernel void square(
device const float *a [[buffer(0)]],
device float *b [[buffer(1)]],
uint id [[thread_position_in_grid]])
{
b[id] = a[id] * a[id];
}
"""
pipeline = device.compile(source, "square")
a = device.buffer(np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32))
b = device.empty(4, np.float32)
pipeline.run([a, b], grid=4)
print(b.contents) # [1. 4. 9. 16.]
grid may be an int (1D dispatch) or a 3-tuple/list for 2D/3D dispatch.
Threadgroup sizing is computed automatically from the pipeline's
thread_execution_width and max_threads_per_threadgroup.
Reusing buffers in a hot loop
Buffer.contents is a live NumPy view over the same underlying Metal
allocation, not a copy — writing buf.contents[:] = ... updates GPU-visible
memory in place, and reading it back after a wait=True dispatch needs no
reallocation either. For a kernel dispatched repeatedly (e.g. in a while
loop), compile the pipeline and allocate buffers once, then just write/read
.contents each iteration:
pipeline = device.compile(source, "square")
a = device.buffer(np.zeros(4, dtype=np.float32)) # allocated once
out = device.empty(4, np.float32) # allocated once
while running:
a.contents[:] = get_next_input() # in-place write, no realloc
pipeline.run([a, out], grid=4) # wait=True by default
consume(out.contents) # in-place read, no realloc
The out-of-place convenience operators (a + b, operators.sqrt(a),
astype, etc.) don't follow this pattern — each call allocates a fresh
output Buffer internally, which is fine for one-off use but wasteful in a
tight loop. The in-place operators (a += b, a *= 2.0, ...) do reuse a's
own buffer with no extra allocation, if that fits your loop. See
examples/reuse_buffers.py.
Testing
pytest tests/
test_basic.py/test_operators.py— correctness for every operator (arithmetic, scalar broadcasting, comparisons, in-place, reductions), dtype, andastypeconversion, plus error handling for mismatched buffer sizes/dtypes/devices and wrong kernel argument counts.test_async.py—wait=Falsedispatch ordering.test_buffer_reuse.py— in-place.contentswrites and repeated dispatch against the same buffers, without reallocation.test_stability.py— repeated-dispatch and object-lifetime stress tests (regression coverage for the Metal object-ownership rules incsrc/), plus multi-threaded dispatch/compilation tests (Pipeline.runreleases the GIL, so this exercises genuinely concurrent Metal calls).test_pipeline_persistence.py— spawns separate processes to verify the on-disk pipeline binary archive is actually written and read back, and thatDevice.flush_cache()writes it on demand.
Benchmarking
python benchmarks/bench.py
Measures first-dispatch (compile-included) and steady-state warm-dispatch
latency/throughput for every operator across a range of buffer sizes, with
NumPy CPU timings alongside for context. Each run is saved as JSON
(timestamped, tagged with the git commit) under benchmarks/results/ so you
can baseline future changes:
python benchmarks/bench.py --baseline benchmarks/results/<earlier-run>.json
benchmarks/demosaic_bench.py is a separate, more involved benchmark
comparing the edge-aware Bayer demosaicing kernel
(benchmarks/bayer2rgb_ea_kernel.txt) against OpenCV's own
COLOR_Bayer*2BGR_EA (requires pip install -e ".[bench]"), covering both
single-shot dispatch latency and realistic streaming throughput (a rotating
buffer pool pipelining dispatches instead of waiting on every frame).
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mtlpy-0.1.0.tar.gz.
File metadata
- Download URL: mtlpy-0.1.0.tar.gz
- Upload date:
- Size: 196.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4568ca200ac662b2fdcea65d9ec70166b40176d9430e557acde8d98c3c2cf6a
|
|
| MD5 |
684e22f09711840ec98ac8974551fc6e
|
|
| BLAKE2b-256 |
0c3c0246339160899500f8db485bd8e41b4c7bc77ec6c6db1644a0e503e0e006
|
Provenance
The following attestation bundles were made for mtlpy-0.1.0.tar.gz:
Publisher:
release.yml on peyton-howe/mtlpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mtlpy-0.1.0.tar.gz -
Subject digest:
c4568ca200ac662b2fdcea65d9ec70166b40176d9430e557acde8d98c3c2cf6a - Sigstore transparency entry: 2084371936
- Sigstore integration time:
-
Permalink:
peyton-howe/mtlpy@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/peyton-howe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mtlpy-0.1.0-cp314-cp314-macosx_14_0_arm64.whl.
File metadata
- Download URL: mtlpy-0.1.0-cp314-cp314-macosx_14_0_arm64.whl
- Upload date:
- Size: 188.0 kB
- Tags: CPython 3.14, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a080a4499c41ea2461fd3d5bb50e88d6aa5338f00f257c49ee5a815590e81c0
|
|
| MD5 |
4530f2c8f5e9d58d3d25d08d9b32678e
|
|
| BLAKE2b-256 |
82b431fa3468a4d2ec92687dd3c97c72f5dfc9e900be72cf7282a15a7431e9cc
|
Provenance
The following attestation bundles were made for mtlpy-0.1.0-cp314-cp314-macosx_14_0_arm64.whl:
Publisher:
release.yml on peyton-howe/mtlpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mtlpy-0.1.0-cp314-cp314-macosx_14_0_arm64.whl -
Subject digest:
7a080a4499c41ea2461fd3d5bb50e88d6aa5338f00f257c49ee5a815590e81c0 - Sigstore transparency entry: 2084372021
- Sigstore integration time:
-
Permalink:
peyton-howe/mtlpy@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/peyton-howe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mtlpy-0.1.0-cp313-cp313-macosx_14_0_arm64.whl.
File metadata
- Download URL: mtlpy-0.1.0-cp313-cp313-macosx_14_0_arm64.whl
- Upload date:
- Size: 187.8 kB
- Tags: CPython 3.13, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c10ca0470445b98043ccc7219291ad51a5a4d49979bd6f0ada6edcfb683a3442
|
|
| MD5 |
9efaddf4ba36ab5d0bf52c024f6eff38
|
|
| BLAKE2b-256 |
3261b45b2619058b48eb932616a835e29b75d99a79a4f210c31ea9e670094d46
|
Provenance
The following attestation bundles were made for mtlpy-0.1.0-cp313-cp313-macosx_14_0_arm64.whl:
Publisher:
release.yml on peyton-howe/mtlpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mtlpy-0.1.0-cp313-cp313-macosx_14_0_arm64.whl -
Subject digest:
c10ca0470445b98043ccc7219291ad51a5a4d49979bd6f0ada6edcfb683a3442 - Sigstore transparency entry: 2084371978
- Sigstore integration time:
-
Permalink:
peyton-howe/mtlpy@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/peyton-howe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mtlpy-0.1.0-cp312-cp312-macosx_14_0_arm64.whl.
File metadata
- Download URL: mtlpy-0.1.0-cp312-cp312-macosx_14_0_arm64.whl
- Upload date:
- Size: 187.7 kB
- Tags: CPython 3.12, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1247e37517072fd36747af386baef60407f6b5ba008b6b261548217295c5102
|
|
| MD5 |
733083398f287aff0ffb875821315cae
|
|
| BLAKE2b-256 |
4f1456a6d49e1f56541c65a5367d9e768383fef4c4f1274c9e5d18546a136e6d
|
Provenance
The following attestation bundles were made for mtlpy-0.1.0-cp312-cp312-macosx_14_0_arm64.whl:
Publisher:
release.yml on peyton-howe/mtlpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mtlpy-0.1.0-cp312-cp312-macosx_14_0_arm64.whl -
Subject digest:
d1247e37517072fd36747af386baef60407f6b5ba008b6b261548217295c5102 - Sigstore transparency entry: 2084372039
- Sigstore integration time:
-
Permalink:
peyton-howe/mtlpy@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/peyton-howe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mtlpy-0.1.0-cp311-cp311-macosx_14_0_arm64.whl.
File metadata
- Download URL: mtlpy-0.1.0-cp311-cp311-macosx_14_0_arm64.whl
- Upload date:
- Size: 186.7 kB
- Tags: CPython 3.11, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a690f273b7e0b61f60a8c1e6344170365fdead2203685b502b9247b622727ea1
|
|
| MD5 |
59c63d7ae4889071989d4cd0009d50ba
|
|
| BLAKE2b-256 |
46715893b346dda4e9926ff2794203842b34d52687ab2dbda11e93e996d60127
|
Provenance
The following attestation bundles were made for mtlpy-0.1.0-cp311-cp311-macosx_14_0_arm64.whl:
Publisher:
release.yml on peyton-howe/mtlpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mtlpy-0.1.0-cp311-cp311-macosx_14_0_arm64.whl -
Subject digest:
a690f273b7e0b61f60a8c1e6344170365fdead2203685b502b9247b622727ea1 - Sigstore transparency entry: 2084371998
- Sigstore integration time:
-
Permalink:
peyton-howe/mtlpy@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/peyton-howe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mtlpy-0.1.0-cp310-cp310-macosx_14_0_arm64.whl.
File metadata
- Download URL: mtlpy-0.1.0-cp310-cp310-macosx_14_0_arm64.whl
- Upload date:
- Size: 185.6 kB
- Tags: CPython 3.10, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5fd1dad5f16bd9683540317831b72823d46c5d6c1aa5180bbaaabcf8b9efb16
|
|
| MD5 |
dd8d7624eec59148a12c2908466fc5ab
|
|
| BLAKE2b-256 |
6eaac025b2ef5abd230fda5f5d1a09e5bcbe97f99a53d4b0c53733f19eda97bf
|
Provenance
The following attestation bundles were made for mtlpy-0.1.0-cp310-cp310-macosx_14_0_arm64.whl:
Publisher:
release.yml on peyton-howe/mtlpy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mtlpy-0.1.0-cp310-cp310-macosx_14_0_arm64.whl -
Subject digest:
d5fd1dad5f16bd9683540317831b72823d46c5d6c1aa5180bbaaabcf8b9efb16 - Sigstore transparency entry: 2084371960
- Sigstore integration time:
-
Permalink:
peyton-howe/mtlpy@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/peyton-howe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd13fdebc572b6c3c0c938282aa9ec72c0ca0f18 -
Trigger Event:
push
-
Statement type: