gpudb

July 30, 2026 · View on GitHub

DuckDB Community Extension Latest release CI License: Apache-2.0 Platforms GitHub stars

The first SQL execution engine for Apple Silicon GPUs, built as a DuckDB extension that also runs on NVIDIA CUDA. One codebase, two backends, your existing DuckDB queries.

Now an official DuckDB Community Extension — install it straight from any DuckDB ≥ 1.5.5, no flags, no downloads:

INSTALL gpudb FROM community;
LOAD gpudb;
SELECT gpu_sum(value::BIGINT) FROM range(1000000) AS t(value);
-- Real query, real GPU, real result on RTX 4090 + Apple M4 Max
SELECT gpu_sum(l_orderkey) FROM read_parquet('lineitem.parquet');
[gpudb] registered gpu_sum / gpu_min / gpu_max (BIGINT,DOUBLE) streaming aggregates (backend=CUDA)
gpu_sum(l_orderkey)
18005322964949

Apache-2.0 · Early stage (v0.3.0) · Linux + macOS · DuckDB ≥ 1.2


Why this exists

Every standalone GPU database from 2013-2024 was acqui-hired or pivoted (HEAVY.AI → NVIDIA 2025, BlazingSQL dormant, Voltron Data 50% layoff). Building "another GPU SQL engine" is not a viable bet.

What's open in 2026: no published SQL engine targets Apple Silicon GPUs. Sirius (UW + NVIDIA, CIDR 2026) is CUDA-only. cuDF is CUDA-only. So is everything else. Apple Silicon's unified memory architecture (up to 512 GB at 819 GB/s on M3 Ultra) is a genuine architectural advantage that nobody has wired into a database.

gpudb is a DuckDB extension (not a fork, not a new database) that closes that gap with a real dual-backend implementation.

Numbers (RTX 4090 Laptop, sm_89, CUDA 13.0)

WorkloadCPU baselineCUDA coldCUDA residentSpeedup
SUM 100M int6413.8 ms / 54 GiB/s80.6 ms (PCIe-bound)0.04 ms / 1187 GiB/s17.9× over CPU
SUM 6M TPC-H lineitem.l_orderkey0.7 ms / 65 GiB/s4.9 ms0.04 ms / 1187 GiB/s17.9× over CPU
GROUP BY 50M × 1M groups1067 ms (serial)130 msn/a9.6× over CPU
GROUP BY 50M × 10M groups2321 ms188 msn/a13.7× over CPU
GROUP BY 6M TPC-H lineitem (1.5M groups)54.1 ms15.0 msn/a3.6× over CPU

Numbers — Apple Silicon (M4 Max, v0.1.3 Metal vs DuckDB CLI default)

DuckDB CLI uses 16 threads (12 P + 4 E cores) by default on M4 Max. These are vs duckdb -c "SET threads=16", the actual user-visible baseline.

WorkloadDuckDB CPU mtMetal v0.1.3Speedup
TPC-H SF10 multi-agg fusion l_quantity27 ms1.06 ms25.5× 🚀
TPC-H SF10 multi-agg fusion l_extendedprice26 ms1.18 ms22.0× 🚀
TPC-H SF10 multi-agg fusion l_orderkey11 ms1.13 ms9.7× 🚀
SF10 SUM l_quantity HOT5 ms1.16 ms4.3×
TPC-H SF10 GROUP BY l_extendedprice (1.35M unique)113 ms28.9 ms3.9×
500M × 1M GROUP BY synthetic820 ms242 ms3.4×
1B × 1M GROUP BY synthetic2500 ms770 ms3.2×
1B int64 SUM HOT40 ms16.2 ms2.6×
SF10 SUM l_extendedprice HOT5 ms2.23 ms2.2×
SF10 SUM l_orderkey HOT3 ms1.68 ms1.8×
TPC-H SF1 GROUP BY l_orderkey (1.5M unique)8 ms5.71 ms1.40×
TPC-H SF10 GROUP BY l_orderkey (15M unique)56 ms42.95 ms1.30×
TPC-H SF10 GROUP BY l_quantity (50 unique)26 ms372 msCPU 14× ❌ structural

v0.1.3 ships a hybrid Metal GROUP BY that auto-dispatches between a 32K-partition slot-lock hash aggregate (sweet spot at 1024 ≤ unique ≤ 16M) and an optimized multi-pass radix sort. Multi-aggregate fusion (SELECT SUM(x), MIN(x), MAX(x), COUNT(x) FROM t) reads the column once and computes all four in a single Metal pass at ~475 GiB/s (87% of LPDDR5X peak). Full numbers + reproduction in BENCHMARK.md.

Quick start

INSTALL gpudb FROM community;
LOAD gpudb;
SELECT gpu_sum(value::BIGINT) FROM range(1000000) AS t(value);
-- -> 499999500000

Works in any DuckDB ≥ 1.5.5 client (CLI, Python, etc.), signed, no flags needed. Community binaries ship the full Metal backend on Apple Silicon and a clean CPU fallback on Linux (the CUDA backend needs a source build for now). The registry currently serves the v0.1.3 submission; community-extensions PR #2404 bumps it to v0.3.0.

Option B — load a prebuilt release binary

Download the platform binary from the latest release, then:

# Linux (RTX/CUDA)
duckdb -unsigned -c "LOAD '/path/to/gpudb.linux_amd64.duckdb_extension'; \
  SELECT gpu_sum(value::BIGINT) FROM range(1000000) AS t(value);"
# -> [gpudb] registered gpu_sum / gpu_min / gpu_max (BIGINT,DOUBLE) streaming aggregates (backend=CUDA)
# -> 499999500000

Requires DuckDB ≥ 1.2 (C API v1.2.0); release binaries track the latest tag. LOAD needs -unsigned here because release-page binaries are unsigned — the community install above doesn't.

Option C — build from source

git clone https://github.com/singhpratech/duckdbgpumetaldbram.git
cd duckdbgpumetaldbram

# Linux (CUDA): one-time toolkit install if needed
# sudo apt install -y cuda-toolkit-13-0
# export PATH=/usr/local/cuda/bin:$PATH

# macOS (Metal): brew install cmake

# build (auto-detects CUDA on Linux, Metal on macOS, CPU-only otherwise).
# Produces a loadable .duckdb_extension with metadata footer attached.
./scripts/build.sh

# load + query via DuckDB CLI
duckdb -unsigned -c "LOAD '$(pwd)/build-linux/src/extension/gpudb.linux_amd64.duckdb_extension'; \
  SELECT gpu_sum(range::BIGINT) FROM range(1000000);"

# OR run via the embedded SQL CLI shipped in this repo
./build-linux/bin/gpudb-sql --sql "SELECT gpu_sum(range::BIGINT) FROM range(1000000);"

Option D (TPC-H reproducibility)

# get TPC-H SF1 data (downloads DuckDB CLI to .tools/, ~1 GB lineitem)
SF=1 ./scripts/gen_tpch.sh

duckdb -unsigned -c "LOAD '$(pwd)/build-linux/src/extension/gpudb.linux_amd64.duckdb_extension'; \
  SELECT gpu_sum(v) FROM read_parquet('data/tpch_sf1/lineitem_orderkey.parquet') t(v);"
# -> 18005322964949

What you get

After build, five CLI tools:

ToolWhat it does
gpudb-sqlEmbeds DuckDB, registers gpu_sum / gpu_min / gpu_max, runs SQL from --sql or stdin. Demo this.
gpudb-benchMicrobench SUM/MIN/MAX across CPU + CUDA + Metal, cold vs hot resident, on synthetic or .gpudb files
gpudb-groupby-benchMicrobench GROUP BY hash aggregate at varying cardinality
gpudb-window-benchMicrobench window functions (running sum, partitioned, unbounded frame)
gpudb-hashjoin-benchMicrobench inner equi-join build × probe across CPU + CUDA

And a static library libgpudb you can embed in any C++ project. See src/extension/gpu_sum_extension.{cpp,hpp} for the DuckDB-aware wrapper.

Architecture

┌──────────────────────────────────────────┐
│  DuckDB (host)                           │
│  Parser → Optimizer → Plan → Executor    │
│            │                             │
│            ↓ aggregate function call     │
│  ┌────────────────────────────────────┐  │
│  │  gpudb extension                   │  │
│  │  - gpu_sum / gpu_min / gpu_max     │  │
│  │  - streaming aggregate states      │  │
│  │      ↓ (operator-level / join)     │  │
│  │  ┌───────────────────────────────┐ │  │
│  │  │  libgpudb backend dispatch    │ │  │
│  │  │  ┌───────┐ ┌──────┐ ┌──────┐  │ │  │
│  │  │  │ CUDA  │ │Metal │ │ CPU  │  │ │  │
│  │  │  └───────┘ └──────┘ └──────┘  │ │  │
│  │  └───────────────────────────────┘ │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘

Backend selection is automatic: CUDA if a device is found at runtime, else Metal if compiled-in, else CPU.

As of v0.3.0 the SQL aggregate path is deliberately CPU-shaped: the aggregates stream running accumulators (the same shape as native DuckDB) instead of shipping chunks to the GPU — the v0.2.0 end-to-end numbers in BENCHMARK.md showed why buffering-for-GPU can't win through this interface on unified memory. The GPU backends serve the operator-level tools above and the SQL join path in development.

Testing

./build-linux/test/test_gpudb        # 96 unit checks across the backends present at build time
./scripts/run_sql_tests.sh           # SQL-level suite: gpu_sum / min / max / GROUP BY / window
./scripts/local_check.sh             # everything CI would run, end to end

The SQL test suite lives in test/sql/*.test. Each file is plain SQL with -- expect: lines after each query; the runner reports per-query PASS / FAIL / XFAIL (expected fail) / SKIP. As of v0.3.0: 60 / 60 pass, 0 fail, 0 expected fail. The window-function bugs that were previously xfail are now strict positive assertions (PR #22).

Reproducibility entry point: scripts/local_check.sh runs the full pipeline end-to-end (configure → build → 96 unit tests → smoke benchmarks → 60-query SQL suite). The hosted CI workflow lives at .github/workflows/ci.yml (Linux + macos-15) and runs on every push to main.

Roadmap

Shipped on main (v0.1.0 – v0.1.2)

  • CUDA backend: SUM/MIN/MAX (one-shot + resident)
  • CUDA GROUP BY hash aggregate (open-addressing + atomicCAS, ~520 GiB/s on RTX 4090)
  • CUDA hash join probe (1M build × 10M probe @ 97% sel: 3.7× wall, 107× kernel over CPU)
  • Metal backend: SUM/MIN/MAX i64 with real compute pipelines (~470 GiB/s on M4 Max)
  • Metal GROUP BY via GPU-resident radix sort (wins 4.4–4.8× over CPU at 100M-500M × 1M groups)
  • Multi-aggregate fusion (SUM+MIN+MAX+COUNT in one pass, 5.3× over CPU fused)
  • Hybrid CPU/GPU planner (HybridAggregator + DispatchDecision, beats both pure-CPU and pure-GPU at the 1M×1M sweet spot)
  • DuckDB extension: gpu_sum / gpu_min / gpu_max with NULL handling + GPUDB_FORCE_BACKEND env var
  • CLI: gpudb-bench, gpudb-groupby-bench, gpudb-window-bench, gpudb-hashjoin-bench, gpudb-sql

Shipped in v0.1.2

  • All 4 known window/GROUP BY bugs fixed (PR #18, #20, #21, #22 — see KNOWN_ISSUES.md)
  • DuckDB loadable extension actually loadsduckdb -unsigned -c "LOAD '/path/to/gpudb.<platform>.duckdb_extension'" works on Linux (CUDA) and macOS (Metal).

Shipped in v0.1.3

  • Hybrid Metal GROUP BY — 32K-partition slot-lock + radix-opt with auto-dispatch (env override GPUDB_METAL_GROUPBY_PATH). Flipped TPC-H SF10 l_orderkey (15M unique) from CPU 1.78× faster to Metal 1.30× faster vs DuckDB CPU 16-thread. 9 wins / 1 honest loss on the lineitem scorecard.
  • Prebuilt v0.1.3 binaries (Linux CUDA + macOS Metal) attached to the v0.1.3 release.

Shipped in v0.2.0

  • SQL-correct NULL semantics (PR #44) — gpu_sum/gpu_min/gpu_max over empty or all-NULL input now return SQL NULL (not 0), matching native DuckDB on every path: plain aggregate, GROUP BY groups, and window frames.
  • gpu_sum(DOUBLE) -> DOUBLE (PR #45) — a real second overload via the C API aggregate function set. Doubles ride the existing int64 state machinery as raw bit patterns (zero state-layout change); only the finalize differs. INTEGER/SMALLINT/TINYINT work via DuckDB's implicit widening to the BIGINT overload (locked in by tests). Type matrix in KNOWN_ISSUES.md.

Shipped in v0.3.0

  • Streaming aggregate states — the SQL aggregate path rewritten from "buffer every value, reduce at finalize" to running accumulators, the same algorithmic shape as native DuckDB. End-to-end on rewritten TPC-H Q6/Q1 and high-cardinality GROUP BY: parity with native (the v0.2.0 buffered path lost 3×–110×; the SF10 GROUP BY cell alone went from 11.05 s to 0.110 s). Full before/after in BENCHMARK.md. GPUDB_FORCE_BACKEND is a no-op on this path now (it routed the deleted machinery).
  • gpu_min(DOUBLE) / gpu_max(DOUBLE) — all three aggregates are now overload sets carrying (BIGINT)->BIGINT and (DOUBLE)->DOUBLE. No backend-interface change was needed under the streaming design. NaN ordering matches native (NaN sorts greatest). Type matrix in KNOWN_ISSUES.md.

Shipped: DuckDB Community Extension

In flight

  • Community Extensions PR #2404 — bump the community build from the frozen v0.1.3 submission to v0.3.0 (in review; pre-validated green on all four platforms).
  • Real Metal hash join + on-device segment reduce + gpu_inner_join — contributed by @lmangani in PR #43 (verified 9.9× on a 1M×10M inner join on M4 Max); landing after a rebase/split pass.

Roadmap (v0.4.0)

  • GPU join as the SQL-path GPU story — land and extend PR #43's join stack; benchmark against DuckDB's 16-thread hash join end-to-end.
  • Community packaging phase 2 — add cuda to requires_toolchains so Linux community binaries ship the CUDA backend.

Beyond (v0.5.0+)

  • Resident-column SQL hooks: gpu_cache(table, col) table function so gpu_sum can run on data already loaded
  • Window functions on GPU as proper operators (not just aggregate-as-window)
  • String / regex operators (libcudf-class functionality on Metal where it doesn't exist)
  • Transparent GPU operator substitution — blocked upstream: the DuckDB C API exposes no optimizer/planner/physical-operator hooks, so a loadable extension cannot silently replace plan operators today. A table-function-based gpu_group_by(...) is possible but doesn't compose with normal SQL; parked until the C API grows the needed hooks.

Why DuckDB? Why not a new database?

The 2013-2024 GPU-DB graveyard is real. The wedge that isn't in the graveyard:

  1. Apple Silicon backend — empty field, defining differentiator
  2. DuckDB-native — no migration, just LOAD
  3. Hybrid CPU/GPU planner — picks CPU when it wins (low cardinality), GPU when it doesn't
  4. Window functions — Sirius lacks them; high-value for analytics

This combination is unique as of May 2026. See GOAL.md for the full positioning and BENCHMARK.md for reproducible numbers.

Citing

If you use this project in research or commercial work:

gpudb: GPU-accelerated DuckDB extension for NVIDIA CUDA and Apple Silicon Metal.
2026. https://github.com/singhpratech/duckdbgpumetaldbram

Author / blog

Build process, design tradeoffs, and ongoing benchmarks are posted at theaivibe.org.

License

Apache-2.0. See LICENSE.