Back to Blog

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

Prateek SinghSeptember 8, 202611 min read
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.

One memory, two processors, the whole of Arrow compute in between Swift · Python · C · Rust · Go · TypeScript · R seven bindings, each tested against its own language's Arrow library one C ABI ArrowMetal 0.1.0 expression compiler → one fused Metal kernel · lazy engine → one GPU round trip Parquet reader on the GPU · out-of-core streaming for tables larger than memory 307 of 307 Apache Arrow v25 compute functions 283 on the GPU · 17 on the host · 7 with a stated limitation validity bitmaps · offsets · dictionaries · nested types CPU · 16 cores a valid Arrow buffer Apple GPU · Metal the same bytes, a valid Metal buffer one unified memory · shared storage · nothing to upload, nothing to download Arrow C Data · C Device · C Stream ⇄ pyarrow · Polars · DuckDB · arrow-rs · arrow-swift
Where ArrowMetal sits. Seven language bindings call one C ABI. Underneath, an expression compiler, a lazy engine, a GPU Parquet reader and out-of-core streaming drive 307 Arrow compute kernels over buffers that live in Metal shared memory, so the CPU and the GPU read the same bytes. Arrow's C Data, C Device and C Stream interfaces connect it to pyarrow, Polars, DuckDB, arrow-rs and arrow-swift.

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:

OperationArrowMetal ms (CPU-ms)fastest CPU idiomits ms (CPU-ms)its coresratio
take, 25M random indices5.90 (0.8)pyarrow143 (143)1.024.2x
string contains, literal (10M)1.64 (0.4)Polars lazy11.47 (173)15.07.0x
sort float6432.12 (1.3)Polars133 (1,284)9.74.1x
group-by sum, 1000 keys4.89 (1.2)pyarrow Acero18.45 (250)13.63.8x
sum int64, 10% nulls1.11 (0.4)Polars lazy2.30 (26)11.12.1x
filter int64, 30% kept2.23 (0.7)Polars lazy2.85 (36)12.61.3x
filter two columns + sum3.53 (1.7)Polars lazy2.92 (41)13.90.83x
temporal year26.82 (0.4)pyarrow Acero17.91 (246)13.70.67x
ln (float64)81.31 (0.5)Polars lazy8.46 (120)14.20.10x
shift, lag 13.58 (0.9)Polars lazy0.03 (0)2.20.01x
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. Baseline = the fastest of each library's eager call and its most parallel idiom. docs/BENCHMARKS_MATRIX.md, generated 2026-09-07.
Wall-time ratio vs the fastest CPU idiom · Apple M4 Max · 50M rows · log axis 1x = tie 0.01x 0.1x 1x 10x take, 25M random indices 24.2x string contains, literal (10M) 7.0x sort float64 4.1x group-by sum, 1000 keys 3.8x sum int64, 10% nulls 2.1x filter int64, 30% kept 1.3x filter two columns + sum 0.83x · to improve temporal year 0.67x · to improve ln (float64) 0.10x · to improve shift, lag 1 0.01x · to improve
The ratio column of the table above on a logarithmic axis. Amber bars are where ArrowMetal is ahead of the fastest CPU idiom; the four dark bars are rows to improve, each with its cause in the next section. Apple M4 Max, 50 million rows, 10 million for the string row.

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: take 24.2x, lexsort 24.0x, group-by sum 3.8x at a thousand groups and 6.3x at a hundred thousand, argsort 7.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. ln at 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 unique and value_counts rows against a threaded hash aggregation, 1 grouped moment.
  • 4 where the CPU library returns a view for free and ArrowMetal materialises a column. shift at 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

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

No spam — just new posts. One-click unsubscribe.
Share this article

Related Posts