Apache Arrow Compute on the Apple Silicon GPU: The First Arrow Project I Created That Does It, With 173 Operations Measured Against Polars, pyarrow and pandas
I built ArrowMetal, the first Apache Arrow project I could find that runs compute on the Apple silicon GPU. Apple silicon has one memory for CPU and GPU, and an Arrow buffer in shared Metal memory is already a GPU buffer; no Arrow project used that. ArrowMetal does: 307 of Arrow's 307 compute functions, seven languages, take at 24.2x pyarrow on an M4 Max, and 339 benchmark rows against the fastest CPU idiom of Polars, pyarrow, pandas and numpy, including the 77 where the CPU is still ahead.
I built ArrowMetal, the first Apache Arrow project I could find that runs compute on the Apple silicon GPU. Every Mac since 2020 has a GPU that shares physical memory with the CPU. An Apache Arrow buffer placed in a Metal buffer with shared storage is, at the same moment, a valid CPU Arrow buffer and a valid GPU buffer. Nothing to upload, nothing to download. When I surveyed the Arrow ecosystem, no project used that: arrow-swift has the types and IPC but no compute kernels; nanoarrow's device layer wraps Metal buffers in C and stops there; cuDF is a full GPU dataframe, on CUDA only; MLX is fast unified-memory tensors with no nulls and no columnar semantics. The hole was the whole of Arrow compute, on the GPU every Mac already has.
ArrowMetal 0.1.0 fills it: all 307 of Arrow's compute functions, seven languages, and 339 benchmark rows against the fastest CPU idiom of Polars, pyarrow, pandas and numpy on an M4 Max, with take at 24.2x pyarrow and sorts, group-bys and string predicates behind it. It is on PyPI, crates.io and GitHub today, Apache-2.0, and listed on Apache Arrow's Powered By page. It is an independent project that implements Apache Arrow, not an Apache project. This post is what it is, what was measured, and where it is not ahead yet.
What ArrowMetal is: Apache Arrow arrays in Metal shared memory
Arrow arrays whose buffers live in Metal shared memory, GPU kernels that honour Arrow semantics (validity bitmaps, offsets, dictionaries, nested types), and the standard Arrow C interfaces in and out, so it plugs into pyarrow, Polars, DuckDB, arrow-rs, arrow-swift or anything else that speaks the C Data Interface. When the producer's buffers are page aligned the GPU uses them in place; when they are not, one copy on the way in.
On top of the kernels: an expression compiler that fuses a whole expression into one generated Metal kernel, a lazy engine so a chain of operations shares one GPU round trip, a Parquet reader on the GPU, and out-of-core streaming for tables larger than memory. All of it sits behind one C ABI, and seven languages bind to it: Swift, Python, C, Rust, Go, TypeScript and R, each with its own test suite against that language's Arrow library.
Coverage is complete by name: all 307 of Apache Arrow v25's compute function names, 283 on the GPU, 17 on the host, 7 with a stated limitation. The table is generated from a registry the test suite executes against pyarrow.compute, so it cannot drift from the code.
Benchmarks: ArrowMetal vs Polars, pyarrow, pandas and numpy on an M4 Max
The comparison is against the fastest thing the CPU has, not a convenient baseline. Every CPU library was measured twice: its plain eager call, and the most parallel idiom it offers for the same answer. Polars through its lazy engine, pyarrow through an Acero plan over sixteen batches, pandas and numpy as they are. pandas' threaded paths, numexpr and numba, were not installed for this run. The baseline in every row is the fastest of all of those, named, with the cores that call used recorded beside it.
Apple M4 Max, 16 CPU cores, 50,000,000 rows (10,000,000 for the string row), best of up to five calls after a warm-up, release build. Wall time in milliseconds, CPU time consumed in parentheses:
| Operation | ArrowMetal ms (CPU-ms) | fastest CPU idiom | its ms (CPU-ms) | its cores | ratio |
|---|---|---|---|---|---|
| take, 25M random indices | 5.90 (0.8) | pyarrow | 143 (143) | 1.0 | 24.2x |
| string contains, literal (10M) | 1.64 (0.4) | Polars lazy | 11.47 (173) | 15.0 | 7.0x |
| sort float64 | 32.12 (1.3) | Polars | 133 (1,284) | 9.7 | 4.1x |
| group-by sum, 1000 keys | 4.89 (1.2) | pyarrow Acero | 18.45 (250) | 13.6 | 3.8x |
| sum int64, 10% nulls | 1.11 (0.4) | Polars lazy | 2.30 (26) | 11.1 | 2.1x |
| filter int64, 30% kept | 2.23 (0.7) | Polars lazy | 2.85 (36) | 12.6 | 1.3x |
| filter two columns + sum | 3.53 (1.7) | Polars lazy | 2.92 (41) | 13.9 | 0.83x |
| temporal year | 26.82 (0.4) | pyarrow Acero | 17.91 (246) | 13.7 | 0.67x |
| ln (float64) | 81.31 (0.5) | Polars lazy | 8.46 (120) | 14.2 | 0.10x |
| shift, lag 1 | 3.58 (0.9) | Polars lazy | 0.03 (0) | 2.2 | 0.01x |
Read the CPU-ms column as well as the wall column. The sort row is 32 ms of wall and 1.3 ms of CPU on the GPU side against 133 ms of wall and 1,284 ms of CPU on the CPU side. While the GPU sorts, the sixteen cores are free for the rest of the application. That is the second result of the project, and it holds on the rows the CPU wins too: the same rows cost 26 to 1,284 CPU-ms on the other side against 0.4 to 1.7 here.
Across the whole matrix, 339 rows over 173 operations:
- 145 rows at or above 3x. These are gathers, group-by, sorts and string predicates:
take24.2x,lexsort24.0x, group-by sum 3.8x at a thousand groups and 6.3x at a hundred thousand,argsort7.7x, the four GPU string predicates 1.6x to 7.0x. The group-by family is 65 of 84 rows at or above 3x. - 102 rows between 1x and 3x. Mostly single passes that read one column and write one; eleven to fifteen CPU cores reach the same unified memory bandwidth the GPU does, and the ratio settles near 1.
- 77 rows to improve, where the fastest CPU idiom is ahead. Each has its cause written down.
- 15 rows with no CPU equivalent.
The 77 rows to improve, by cause
A benchmark table that only shows wins is an advertisement. These are the rows where the CPU idiom is ahead, grouped the way docs/TO_IMPROVE.md groups them:
- 16 at a million rows and below, where the dispatch floor is the whole operation. A GPU launch costs what it costs; below a few million rows the CPU has finished before the GPU has started. The pipelining plan for this is in the design notes.
- 28 memory-bound single passes, where twelve to fifteen cores reach the same memory.
- 6 software binary64 transcendentals and calendar arithmetic.
lnat 0.10x is this: Apple GPUs have no hardware float64 transcendentals, so they are emulated. - 8 temporal field extractions, 4 string conversions with variable-length output, 4
uniqueandvalue_countsrows against a threaded hash aggregation, 1 grouped moment. - 4 where the CPU library returns a view for free and ArrowMetal materialises a column.
shiftat 0.01x is this: Polars slices, ArrowMetal writes 50 million values. - 2 regular expressions matched on the host, 4 whole-query chains against a streaming engine.
Every one of them is in the repository with its number and what would change it.
Bugs found upstream in Apache Arrow, Arrow Go, Arrow JS and Swift
Building seven bindings against seven Arrow libraries and running 39,069 differential cases against pyarrow turns up bugs that are not yours. Fourteen findings so far are reported upstream with a reproduction each: a binary_slice overflow and a utf8_normalize that never composed (both apache/arrow, the second with a fix pull request open), three in Arrow Go, one in Arrow JS, one in the Swift compiler at -O with a 22-line two-module reproduction, and an R pull request that exports the C Data Interface allocators arrow's R package kept internal. The tracker on arrowmetal.org shows where each one stands, updated from the projects' own issue trackers twice a day.
Install ArrowMetal: pip, cargo, Swift, Go, R, TypeScript and C
pip install arrowmetal # macOS arm64; pyarrow arrays in, pyarrow arrays out
import pyarrow as pa, arrowmetal as am
a = am.array(pa.array(range(50_000_000)))
a.sum() # on the GPU, no copy if the buffer is page aligned
Rust: arrowmetal = "0.1.0" on crates.io. Swift, Go, R, TypeScript and C are in the repository with their own instructions. Everything in this post, the benchmark CSVs and the script that regenerates the tables included, is at github.com/singhpratech/ArrowMetal. The site is arrowmetal.org.
What is next for Apache Arrow on Apple GPUs
The dispatch floor is the biggest single cause in the 77, and the design has a pipelining plan for it. Benchmark files from machines that are not an M4 Max are the thing the project most needs; the matrix script accepts them and the site renders them. And the upstream reports keep going: each one that lands upstream removes a workaround here.
FAQ: Apache Arrow on Apple silicon GPUs
Is ArrowMetal an Apache project?
No. ArrowMetal is an independent, Apache-2.0-licensed project that implements Apache Arrow. It is listed on Apache Arrow's Powered By page, which is a directory of projects that use Arrow, not a list of Apache projects.
What does ArrowMetal run on?
Apple silicon Macs, whose GPU shares physical memory with the CPU. The Python wheel is built for macOS arm64. Rust, Swift, Go, R, TypeScript and C bindings are in the repository.
Does ArrowMetal copy data to the GPU?
Not when the producer's buffers are page aligned: an Arrow buffer in Metal shared memory is used by the GPU in place. When they are not aligned, there is one copy on the way in and none on the way out.
How much of Apache Arrow compute does ArrowMetal cover?
All 307 of Apache Arrow v25's compute function names: 283 run on the GPU, 17 on the host, and 7 carry a stated limitation. The coverage table is generated from a registry the test suite executes against pyarrow.compute.
How does ArrowMetal compare with Polars and pyarrow?
On an Apple M4 Max at 50 million rows, take is 24.2x pyarrow, a literal string contains is 7.0x Polars lazy, a float64 sort is 4.1x Polars and a 1,000-key group-by sum is 3.8x pyarrow Acero. Of 339 measured rows, 145 are at or above 3x, 102 sit between 1x and 3x, and 77 are rows where the fastest CPU idiom is still ahead, such as ln at 0.10x and shift at 0.01x.
How do I install ArrowMetal?
Python: pip install arrowmetal (macOS arm64). Rust: add arrowmetal = "0.1.0" from crates.io. Swift, Go, R, TypeScript and C have their own instructions in the GitHub repository.
More of my Apache Arrow and Apple silicon work
- gpudb: a SQL engine on Apple silicon GPUs, now a DuckDB community extension
- Why I built gpudb, a GPU SQL engine for CUDA and Apple silicon
- adbcBridge: an Apache Arrow ADBC driver for every ODBC database
- adbcBridge Part 2: the ODBC driver bugs it found upstream
I also built adbcBridge, an independent, Apache-2.0-licensed implementation of the ADBC standard for any ODBC database; that post is here on the same site.
References & Citations
- ArrowMetal (2026). “README: What was measured, Benchmarks, Why this exists.” github.com/singhpratech/ArrowMetal, main, read 8 September 2026.
- ArrowMetal (2026). “Benchmarks matrix.” docs/BENCHMARKS_MATRIX.md, generated 7 September 2026 — Apple M4 Max, 16 cores, 64 GB; Polars 1.44.1, pyarrow 25.0.1, pandas 3.0.5, numpy 2.5.3. Source of every number in the headline table and the 339-row distribution.
- ArrowMetal (2026). “To improve.” docs/TO_IMPROVE.md — the 77 rows where the fastest CPU idiom is ahead, grouped by cause.
- ArrowMetal (2026). “Upstream.” docs/UPSTREAM.md — the fourteen findings reported to other projects, with reproductions.
- ArrowMetal (2026). “Testing.” docs/TESTING.md — the 39,069 differential cases against pyarrow and the per-language suites.
- ArrowMetal (2026). Release v0.1.0 on GitHub; arrowmetal 0.1.0 on PyPI; arrowmetal and arrowmetal-sys 0.1.0 on crates.io. All verified live 8 September 2026.
- Apache Arrow. “Powered By.” arrow.apache.org — lists ArrowMetal. ArrowMetal is an independent Apache-2.0 project that implements Apache Arrow; it is not an Apache project.
- apache/arrow. PR #51237 [C++] utf8_normalize: compose for NFC and NFKC; PR #51236 [R] Export the C Data Interface allocators. Both open 8 September 2026.
- Apache Arrow. “The Arrow C data interface.” arrow.apache.org/docs/format.
- arrowmetal.org — coverage table, benchmark rounds and the upstream tracker.
Subscribe to new posts from theaivibe.org
Related Posts

Polars vs DuckDB vs ArrowMetal GPU on Apple Silicon: Sort and Group-By Benchmarks
Polars, DuckDB and ArrowMetal on an Apple M4 Max: sort and group-by benchmarks at 10M and 50M rows, wall time next to CPU time, and the rows where the CPU is still ahead.

“PostgreSQL-compatible” Is Not PostgreSQL: What Arrow's Native ADBC Driver Does on 14 Wire-Compatible Databases
I ran the native PostgreSQL and MySQL ADBC drivers against 28 databases that speak their protocols. Half stopped. Then I found a bug in my own driver.

Apache Arrow ADBC Just Listed My ODBC Bridge on Its Official Integrations Page — Seven Days After v0.1.0
The Apache Arrow ADBC documentation now lists adbcBridge on its Tools & Integrations page — seven days after I released v0.1.0. I filed the listing request on August 29; on August 31 a project member invited a PR, and it was merged six hours after the invitation. What the entry says, how the week that earned it went, and what it changes for anyone with an ODBC-only database.