The First SQL Engine for Apple Silicon GPUs Is Now a DuckDB Community Extension
In May 2026 I shipped gpudb v0.1 — the first SQL execution engine targeting Apple Silicon GPUs, built as a DuckDB extension with a CUDA backend on Linux. Three releases later, the project crossed two lines at once. v0.3.0's streaming-aggregate rewrite reached parity with native DuckDB on end-to-end TPC-H queries — the worst cell improved roughly 100×, from 11.05 s to 0.109 s. And gpudb became an official DuckDB Community Extension: INSTALL gpudb FROM community now works in any DuckDB ≥ 1.5.5, signed, no flags. This is the full arc — what v0.1 proved, what v0.2 honestly lost, what v0.3 fixed, and why the next GPU frontier is joins.
TL;DR — gpudb, the first SQL execution engine for Apple Silicon GPUs, is now an official DuckDB Community Extension. In any DuckDB ≥ 1.5.5: INSTALL gpudb FROM community; — signed, no flags, full Metal backend on M-series Macs. v0.3.0's streaming rewrite reached parity with native DuckDB end-to-end (worst TPC-H cell improved ~100×), while the Metal operator kernels keep their 3–25× wins. Next up: a community-contributed GPU hash join, verified 9.9×.
In May 2026 I shipped something that, as far as I can tell, had never existed: a SQL execution engine that runs on Apple Silicon GPUs. Not a paper, not a prototype in a notebook — a loadable DuckDB extension with real Metal compute pipelines, benchmarked on TPC-H, with a CUDA backend riding the same codebase on Linux. That was gpudb v0.1.0, and the claim was narrow but real: nobody had wired a Mac's GPU into a database before.
Three months and three releases later, two lines crossed at once. The v0.3.0 rewrite reached parity with native DuckDB on end-to-end TPC-H queries — the worst cell improved by roughly 100×. And gpudb became an official DuckDB Community Extension, which means the entire install procedure is now:
INSTALL gpudb FROM community;
LOAD gpudb;
Any DuckDB ≥ 1.5.5, signed binaries, no flags, no downloads. This post is the story of how it got from first kernel to one-line install — including the release where the honest answer was "the SQL path loses by 109×," because that release is the reason the current one works.
Why an extension, and why Apple Silicon
The GPU-database idea is not new. It is, in fact, a graveyard. MapD became OmniSci became HEAVY.AI and was acqui-hired by NVIDIA in 2025. BlazingSQL went dormant in 2021. Voltron Data cut half its staff in 2025. Sirius — the strongest current academic entry, out of UW with NVIDIA backing at CIDR 2026 — is CUDA-only. Every standalone GPU database asked users to migrate their data and their queries to a new engine, and users kept declining.
gpudb makes two bets against that history. First: be an extension, not a database. DuckDB is already on the machines that matter — analysts' laptops, CI boxes, notebooks — and a loadable extension means zero migration: your files, your SQL, one LOAD. Second: target the hardware nobody else targets. Every engine in that graveyard, and every survivor, is CUDA-only. Meanwhile Apple ships up to 512 GB of unified memory at 819 GB/s on the M3 Ultra — a memory subsystem that would embarrass most server hardware — and no SQL engine had ever touched its GPU.
What v0.1 proved: the Apple Silicon GPU is a real analytics device
The first release was about existence proofs, and the numbers were better than I expected. On an M4 Max, against DuckDB's own CPU engine using all 16 threads — the honest baseline, not a strawman single-threaded loop — the Metal operator scorecard came back 9 wins, 1 loss on TPC-H lineitem cells.
The headline is the top of that chart: multi-aggregate fusion. Most real analytical queries don't compute one aggregate — they compute several over the same column (SELECT SUM(x), MIN(x), MAX(x), COUNT(x) is the median TPC-H pattern). Executed naively, that reads the column from DRAM once per aggregate. The fused Metal kernel reads it once and computes all four in a single pass, sustaining about 470 GiB/s on M4 Max — roughly 87% of the machine's memory bandwidth. Fusing four operations into one pass costs zero extra bandwidth, so per-op throughput effectively quadruples. That is where the 22–25× cells come from.
And the honest loss stays on the card: a GROUP BY with only 50 unique values loses to the CPU by 14×, because the CPU keeps its entire hash table in L1 cache and no GPU dispatch can pay for itself. That's not a bug to fix — it's a boundary to respect, which is why gpudb ships a hybrid planner that routes low-cardinality shapes to the CPU and keeps the GPU for the shapes it wins.
The Linux side: same codebase, opposite physics
The CUDA backend tells the same story from the other direction. On an RTX 4090, a resident-column SUM over 100 million rows sustains 1187 GiB/s — 17.9× over the CPU, finishing in 0.04 ms. But run it cold, and the PCIe transfer costs 80 ms before the 0.04 ms kernel even starts.
On a discrete GPU the transfer is the enemy, so the design wants resident columns. On unified memory the transfer is free — but so is the CPU's access to the same bytes, so the design wants compute-dense operators. One codebase, two backends, and the two lessons turn out to be mirror images of each other. Which brings us to the release where I had to learn that the hard way.
v0.2: the release that lost, on the record
The operator scorecard above is measured at operator level — the kernels against the CPU doing the same work. v0.2.0 asked the harsher question: what happens end-to-end, through the DuckDB CLI, on real rewritten TPC-H queries, extension versus native? The answer, recorded in BENCHMARK.md with a straight face: the extension lost every cell, by 3× to 109×.
The cause wasn't the GPU — it was the plumbing around it. The v0.2 aggregate path buffered every incoming value into per-state vectors and reduced at finalize. On unified memory, that copy is the whole cost: the query spends its time duplicating a column DuckDB already holds in memory, then a fast reduction at the end saves nothing. The 4-point analysis in the benchmark log predicted the loss was structural, not tunable. It was right.
v0.3: parity, by deleting work instead of adding it
The v0.3.0 rewrite replaced the buffered path with streaming running accumulators — the same algorithmic shape as a native DuckDB aggregate. No buffering, no copy, no finalize-time reduction. And on unified memory, one more consequence falls out: for a plain scalar aggregate over a column, the rewritten path doesn't dispatch to the GPU at all, because shipping bytes to a coprocessor that shares your memory just to add them up is pure overhead.
Every cell is now within 0–20% of native — Q6 at SF1 is a dead-even 1.00×, and the worst cell (SF1 GROUP BY) is 1.20×. The SF10 GROUP BY went from 11.05 seconds to 0.109 seconds. Correctness was gated before any timing counted: Q6 revenue totals match native to the printed digit, Q1 row sets match row-for-row, GROUP BY checksums are identical at both scale factors. The remaining 0–20% is recorded as unprofiled, because it is.
I want to be precise about what this means, because it's the most honest sentence in the release: v0.3's parity came from teaching the extension when not to use the GPU. The GPU's wins remain at operator level — the fused multi-aggregates, the large GROUP BYs, the shapes in Fig 4 — and the hybrid planner routes to them. What v0.3 fixed is that using gpu_* aggregates in an ordinary query is no longer a footgun. The extension now costs nothing where the GPU can't help, and pays off where it can.
Distribution: the part indie projects usually never solve
A database extension you have to compile from source, or load with security flags disabled, is a demo. The DuckDB Community Extensions registry is what turns it into software: the DuckDB team's infrastructure builds the extension from source on every platform, signs the binaries, and serves them to every DuckDB client. Two merged PRs later (#1898 for acceptance, #2404 for the version bump), the registry now ships gpudb v0.3.0 on all four platforms — with the full Metal backend in the Apple Silicon build, and a clean CPU fallback elsewhere. If you have DuckDB 1.5.5 or newer on an M-series Mac, you are one SQL statement away from Metal kernels. (The CUDA backend still needs a source build on Linux — toolchain packaging is a v0.4 item.)
What's next: joins, and a contributor I didn't recruit
The best signal a small open-source project can get is a stranger showing up with working code. gpudb's PR #43 is exactly that — a real Metal hash join with an on-device segment reduce and a gpu_inner_join surface, contributed from outside, verified at 9.9× on a 1M × 10M inner join on M4 Max. It's landing after a rebase pass, and it defines the v0.4 arc: joins are compute-dense in precisely the way scalar aggregates aren't, which makes them the right SQL-path GPU story on unified memory. The roadmap after that — resident-column SQL hooks, GPU window operators, and string operators on Metal where nothing like libcudf exists — is in the repo.
Try it
If you have an Apple Silicon Mac and DuckDB ≥ 1.5.5, the whole experiment is thirty seconds:
INSTALL gpudb FROM community;
LOAD gpudb;
SELECT gpu_sum(value::BIGINT) FROM range(1000000) AS t(value);
-- 499999500000, via a Metal streaming aggregate
The repo is github.com/singhpratech/duckdbgpumetaldbram — Apache-2.0, with every number in this post reproducible from BENCHMARK.md, losses included — and the extension's official home is its DuckDB Community Extensions page. The full technical write-up — architecture, kernel design, and the complete benchmark tables with an August 2026 addendum — is in the gpudb paper on this site. Thirteen years of GPU-database history says the standalone play doesn't survive. The bet here is different: ride an engine people already trust, target the hardware nobody else does, and report the 14× losses next to the 25× wins. So far, that bet is one merged registry and one external contributor ahead.
Subscribe to new posts from theaivibe.org
Related Posts

pankhllm: The LLM Gateway That Learns to Skip the LLM, Without Replacing the Stack You Already Run
Most of an agent's LLM calls are not writing anything. They are decisions: which tool, which skill, which parameters, made thousands of times a day by a model paid in seconds and tokens. pankhllm sits where your app already calls an LLM, learns those decisions from its own traffic, and starts making them in 0.2 ms on a CPU with a 262 KB model. What it is not sure about still goes to your LLM. On the same 14 questions: a 12B planner 1,743 ms, Laya 49 ms, pankhllm's own model 4 ms, all 14 correct. Here is what it is, what it is not, and where it stops.
Your Apple Silicon GPU Loses to One CPU Core Until a Million Rows. I Measured 111 Operations, Then Rebuilt ArrowMetal 0.2.0 Around the Answer
Every GPU data library benchmarks itself at 50 million rows. Your dataframe has 80,000. On an Apple M4 Max, summing 1,000 integers takes the GPU 112 microseconds and Polars less than one: the GPU is more than 100 times behind. I built one of these libraries, so I measured the row count where the GPU overtakes the fastest CPU code for 111 operations: the median needs 10,000,000 rows against a multi-core library, about a million against one core, and sixteen never get there. So ArrowMetal 0.2.0 refuses the GPU below the line, byte-identical, and around that router it grew GPU readers for CSV, JSON, nested Parquet, Delta Lake and Iceberg, a Polars engine and a DuckDB optimizer extension.
DuckDB on the Apple Silicon GPU: Plain SQL, 19 of 22 TPC-H Queries on the Mac's Own GPU, and a Rule That Says Never Slower
DuckDB has no GPU backend of its own, and the GPU engines built for it need an NVIDIA card. gpudb 0.7 is my Apache-2.0 DuckDB extension for the GPU already inside your Mac, and for CUDA too. You write plain DuckDB SQL; the GPU takes a statement only where it has been measured faster than DuckDB on your own machine. On an Apple M4 Max, 19 of 22 TPC-H SF10 queries run on the Metal GPU at 1.06x to 48x with zero rows differing. The one row below parity is printed, not dropped.