Back to Blog

The First SQL Engine for Apple Silicon GPUs Is Now a DuckDB Community Extension

Prateek SinghAugust 11, 20268 min read
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.

Three months from first Metal kernel to signed one-line install v0.1.0 v0.1.3 v0.2.0 v0.3.0 community May 9, 2026 first Metal SQL kernels — the Apple Silicon first May 10 · hybrid Metal GROUP BY, 9/10 TPC-H wins Jul 19 · SQL-correct NULLs, honest audit: end-to-end loses Jul 19 · streaming rewrite: native parity end-to-end INSTALL gpudb FROM community registry: v0.3.0
Fig 1 — Three months from the first Metal SQL kernel to a signed one-line install. v0.2.0's honest audit is the hinge of the story.

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.

The standalone GPU-database graveyard, 2013–2026 20132016201920222025 MapD → HEAVY.AI acqui-hired by NVIDIA, 2025 BlazingSQL dormant since 2021 Voltron Data 50% layoff Sirius (CIDR 2026) CUDA-only SQL engines targeting Apple Silicon GPUs before May 2026: none gpudb opened the lane — as a DuckDB extension, not another standalone database Every standalone GPU database was acqui-hired, went dormant, or pivoted. The wedge that survived is an extension riding an engine people already run.
Fig 2 — The standalone GPU-database graveyard, 2013–2026. The Apple Silicon lane was empty until May 2026.

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.

Why Apple Silicon changes the GPU-database math Discrete GPU (every prior engine) CPUsystem RAM GPUVRAM PCIe copy tax on every query SUM 100M int64, cold: 80.6 ms — PCIe-bound Apple Silicon (unified memory) one memory pool — CPU + GPU up to 512 GB @ 819 GB/s (M3 Ultra) no copy — the GPU reads the same bytes M4 Max sustains ~470 GiB/s in gpudb kernels The double edge: unified memory deletes the transfer cost that killed discrete-GPU databases — but it also deletes the excuse. If the CPU reads the same bytes at full bandwidth, the GPU must win on compute per byte. v0.3 is that lesson, learned.
Fig 3 — Unified memory deletes the PCIe copy tax that killed discrete-GPU databases — and with it, the excuse. On UMA the GPU has to win on compute, not memory locality.

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.

Metal v0.1.3 vs DuckDB CPU (16 threads), Apple M4 Max — operator scorecard multi-agg fusion · SF10 l_quantity multi-agg fusion · SF10 l_extendedprice multi-agg fusion · SF10 l_orderkey SUM l_quantity · SF10 hot GROUP BY l_extendedprice · 1.35M unique GROUP BY 500M × 1M synthetic GROUP BY 1B × 1M synthetic SUM 1B int64 hot SUM l_extendedprice · SF10 hot GROUP BY l_orderkey · 15M unique GROUP BY l_quantity · 50 unique 25.5× 22.0× 9.7× 4.3× 3.9× 3.4× 3.2× 2.6× 2.2× 1.3× CPU wins 14× — structural The loss is reported, not hidden: at 50 unique groups the CPU keeps everything in L1 and no GPU dispatch can pay for itself. The hybrid planner exists for exactly this — it routes those shapes to CPU. 9 wins, 1 honest loss on the lineitem card.
Fig 4 — The v0.1.3 Metal scorecard vs DuckDB CPU (16 threads). The 14× loss at 50 unique groups is structural and stays on the card.

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.

The fusion wedge: SUM(x), MIN(x), MAX(x), COUNT(x) — one read, not three separate: 3 passes over DRAM SUM MIN MAX fused: 1 pass, 4 results SUM·MIN·MAX·COUNT 1B rows hot: 48.8 ms → 16.1 ms (3.04×) Sustained kernel bandwidth, M4 Max: ~470 GiB/s peak ≈ 87% of LPDDR5X memory bandwidth — fusing four ops into one pass costs zero extra, so per-op throughput effectively quadruples — the shape of half of TPC-H (Q1, Q6, …).
Fig 5 — The fusion wedge: one pass over DRAM instead of three, at ~87% of the M4 Max memory peak.

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.

The CUDA lane (RTX 4090 Laptop, sm_89) — SUM 100M int64 CPU (DuckDB) 54 GiB/s · 13.8 ms CUDA · resident 1187 GiB/s · 0.04 ms · 17.9× CUDA · cold 80.6 ms — PCIe-bound: the copy costs more than the kernel Same lesson seen from the other side: on a discrete GPU the transfer is the enemy, so the design pushes toward resident columns; on unified memory the CPU reads the same bytes — so the design pushes toward compute-dense operators. One codebase, both.
Fig 6 — The CUDA lane on RTX 4090: 1187 GiB/s resident, PCIe-bound cold. Discrete GPUs push the design toward resident columns; unified memory pushes it toward compute-dense operators.

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.

v0.3.0 streaming rewrite — end-to-end TPC-H vs native (lower = better) Q6 · SF1 Q6 · SF10 Q1 · SF1 Q1 · SF10 GROUP BY · SF1 GROUP BY · SF10 v0.2.0: ~3× slowerv0.3.0: 1.00× ~3.4×1.06× ~30×1.09× ~35×1.03× ~86×1.20× ~109×1.18× SF10 GROUP BY, one cell: 11.05 s → 0.109 s ~100× — identical results Same queries, methodology and machine (M4 Max, DuckDB CLI, 16 threads, median of 5). Correctness gated first: Q6 totals match native to the printed digit, Q1 row sets match row-for-row, GROUP BY checksums identical at both scales. Bars are log-scaled. Worst v0.3.0 cell: 1.20× — recorded, not rounded away.
Fig 7 — v0.2.0's 3×–109× end-to-end losses collapse to 1.00×–1.20× in v0.3.0, with identical results. The SF10 GROUP BY cell alone improved ~100×.

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.

What v0.3 taught us: where the GPU belongs on unified memory bandwidth-bound scalar work one SUM over one column → CPU wins. Don't dispatch. v0.3 streams it like a native aggregate — parity compute-dense operator work fused multi-agg · millions of groups · joins → GPU wins. 3×–25× measured. hybrid planner routes by shape, per cardinality next in line: joins community PR #43 — a real Metal hash join + gpu_inner_join, verified 9.9× on a 1M × 10M inner join on M4 Max — landing in v0.4
Fig 8 — The v0.3 lesson: scalar bandwidth-bound work goes to the CPU, compute-dense operator work goes to the GPU — and joins are the next compute-dense frontier.

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.)

Distribution, solved: the community registry INSTALL gpudb FROM community; LOAD gpudb; -- any DuckDB ≥ 1.5.5 · signed · no flags macOS arm64 — full Metal backend 3 more platforms — CPU fallback Two merged community-extensions PRs (#1898 acceptance, #2404 version bump) — the registry now builds, signs and serves v0.3.0 on all platforms. Already installed? UPDATE EXTENSIONS; pulls the latest. The CUDA backend still needs a source build on Linux (toolchain packaging is a v0.4 item).
Fig 9 — Two merged community-extensions PRs later, the registry builds, signs and serves v0.3.0. Apple Silicon gets the full Metal backend.

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

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

Related Posts