GPU-accelerated plain DuckDB SQL on Apple Silicon Metal and NVIDIA CUDA - GROUP BY, joins, filters and top-k run on the GPU when measured faster, with the same answers. First SQL execution engine that targets Apple Silicon GPUs.
Installing and Loading
INSTALL gpudb FROM community;
LOAD gpudb;
Example
LOAD gpudb;
-- which processor will run, and what this build can do
SELECT gpu_build_info();
-- make a (key, value) pair resident on the GPU once ...
CREATE TABLE sales AS
SELECT (range % 1000)::BIGINT AS store, (range * 7 % 10007)::BIGINT AS amount
FROM range(10000000);
SELECT gpu_upload_pair('sales_by_store', store, amount) FROM sales;
-- ... then ask as often as you like: GROUP BY, HAVING and top-k run on the device
-- top 5 stores by sum(amount): (store, sum, count)
SELECT * FROM gpu_groupby_sum_resident_topk('sales_by_store', 5, 'desc');
-- backend, rows, wall time and kernel time of the last call
SELECT gpu_last_stats();
About gpudb
Plain DuckDB SQL on the GPU
gpudb is DuckDB plus a GPU. You write the SQL you already write. A statement is
answered by the GPU when that has been measured to be faster for its shape and
size on your machine, and by DuckDB — untouched, at its usual speed — otherwise.
Rows, column names and column types are identical either way.
Two rules hold everywhere and are enforced by the test gates the project ships:
- Never slower than DuckDB. Every rewritten shape is timed against native; a shape that loses is left to DuckDB, and the losing measurements stay published.
- Never a different answer. Integer and
DECIMALaggregates are bit-exact (128-bit sums on the device). Anything DuckDB itself computes order-dependently —sum(DOUBLE)— is never rewritten.
Three ways in
The first two put plain SQL on the device and need the Python package as
well as this extension, because DuckDB's stable C API has no hook that sees a
statement before it is planned: pip install duckdb-gpudb. On Apple Silicon
(macOS 15 or later) and x86-64 Linux (glibc 2.34 or newer) that wheel carries
a matching extension binary inside it, which is the quickest way to plain SQL
on the GPU; elsewhere it uses the extension DuckDB has installed from this
registry. The third needs no wrapper: loaded in any DuckDB client, this
extension gives the explicit gpu_* functions.
1. The gpudb shell — a SQL shell whose footer says, under every result,
where the statement ran and why.
$ gpudb my.duckdb
gpudb> SELECT l_partkey, sum(l_quantity) AS qty FROM lineitem
GROUP BY l_partkey ORDER BY qty DESC LIMIT 5;
...
GPU (topk: the resident GROUP BY)
2. Python — gpudb.connect() — the same decision, for applications and
notebooks.
import gpudb
con = gpudb.connect("my.duckdb") # same surface as duckdb.connect()
con.sql("SELECT l_partkey, sum(l_quantity) AS qty FROM lineitem "
"GROUP BY l_partkey ORDER BY qty DESC LIMIT 5")
con.last_rewrite() # what ran where, and why
Tables become resident in the background, in short segments taken only while the connection is idle. A memory budget decides what stays on the device by measured value; what does not fit simply runs on DuckDB.
3. Explicit gpu_* functions — any DuckDB client, including the CLI.
This is the only route that needs no wrapper, because DuckDB's stable C API has no
hook that sees a statement before it is planned. gpu_upload / gpu_upload_pair
make columns resident once; gpu_sum_resident, gpu_groupby_sum_resident
(_having, _topk), gpu_topk_resident, gpu_inner_join and the exact
gpu_groupby_exact_* family read them from GPU memory. gpu_last_stats() reports
which processor ran and for how long.
What runs on the device
| SQL | On the GPU |
|---|---|
GROUP BY with sum count min max avg, count(DISTINCT) |
yes — integer, DATE, TIMESTAMP, DECIMAL, VARCHAR keys, several keys, GROUP BY ALL |
WHERE |
yes — one fused pass on the device |
HAVING, ORDER BY ... LIMIT k |
yes |
| Joins | inner, left, right, many-to-many; several payload columns |
| Subqueries | EXISTS, IN, scalar subqueries as WHERE terms; derived tables; CTEs; views |
Aggregates without GROUP BY |
yes |
Window functions, FULL joins, median / stddev / quantiles, sum(DOUBLE), prepared-statement parameters, statements inside an explicit transaction |
stay on DuckDB |
sum(DOUBLE) stays on DuckDB by design: DuckDB's own result depends on the order the
values are added, so there is no single answer for a GPU to match. The explicit gpu_sum
functions remain available for it, with a stated 1e-9 relative tolerance.
Measured (TPC-H, every row compared with native DuckDB)
Release build of 2026-09-20, default memory budget, N=5. A statement reaches the
GPU through execute() or through sql() — two different code paths, timed
separately:
| asked through | on the GPU | rows differing | speed-up on those queries | |
|---|---|---|---|---|
| Apple M4 Max (Metal), SF1 | execute() | 17 of 22 | 0 | 1.52x - 15.32x |
| Apple M4 Max (Metal), SF1 | sql() | 17 of 22 | 0 | 1.37x - 9.49x |
| Apple M4 Max (Metal), SF10 | execute() | 19 of 22 | 0 | 1.06x - 48.10x |
| Apple M4 Max (Metal), SF10 | sql() | 19 of 22 | 0 | 0.92x - 26.39x |
| RTX 4090 Laptop (CUDA), SF1 | execute() | 17 of 22 | 0 | 1.21x (Q15) - 31.44x (Q9) |
| RTX 4090 Laptop (CUDA), SF1 | sql() | 17 of 22 | 0 | 1.66x (Q15) - 16.27x (Q9) |
Plain SQL runs on the GPU by default on both backends. One row is below 1.0x and is printed rather than dropped: TPC-H Q11 at SF10 on Metal, a 6-7 ms statement measuring 1.06x through execute() and 0.92x through sql(), which straddles parity - the per-process measured rule is what declines such a statement where it loses. Nothing on CUDA is below 1.0x in these runs: Q1 first measured 0.96x and 0.98x there on the release build, the cause was found (a few-group key was being answered by sorting the whole column), CUDA got a direct grouped reduce for such keys, and Q1 now measures 1.99x and 2.07x.
The remaining queries are declined on purpose (correlated subqueries, or shapes
where DuckDB was measured faster) and run on DuckDB unchanged. Every measurement,
including the shapes where the GPU loses, is in BENCHMARK.md; every trade-off
is in KNOWN_ISSUES.md; the dated engineering journal is docs/RESEARCH_NOTES.md.
Platforms
| Platform | From this registry |
|---|---|
| macOS, Apple Silicon | Metal GPU |
| Linux x86_64 / arm64 | SELECT gpu_build_info(); says which backends the binary you received carries and which one it chose. Where it is CPU-only, the SQL surface and the answers are the same. A CUDA build is also available from the project's releases and from source |
| No GPU | CPU fallback |
The extension is written in C++ and touches DuckDB through its stable C API only (the C_STRUCT ABI): it links no libduckdb, includes no DuckDB C++ headers and does no plan surgery.
Guides: the gpudb shell
https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_THE_SHELL.md ,
gpudb.connect()
https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_PYTHON.md ,
installing both pieces
https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/INSTALL.md
Source, benchmarks and issues: https://github.com/singhpratech/duckdbgpumetaldbram
Added Functions
| function_name | function_type | description | comment | examples |
|---|---|---|---|---|
| gpu_agg_exact_global | table | NULL | NULL | |
| gpu_anti_join_count_resident | scalar | NULL | NULL | |
| gpu_anti_join_sum_resident | scalar | NULL | NULL | |
| gpu_anti_join_sum_resident_f64 | scalar | NULL | NULL | |
| gpu_assert_rows | scalar | NULL | NULL | |
| gpu_avg_decimal | scalar | NULL | NULL | |
| gpu_build_info | scalar | NULL | NULL | |
| gpu_drop_column | scalar | NULL | NULL | |
| gpu_drop_resident | scalar | NULL | NULL | |
| gpu_groupby_count_resident | table | NULL | NULL | |
| gpu_groupby_count_resident_having | table | NULL | NULL | |
| gpu_groupby_count_resident_topk | table | NULL | NULL | |
| gpu_groupby_exact_multi | table | NULL | NULL | |
| gpu_groupby_exact_resident | table | NULL | NULL | |
| gpu_groupby_exact_resident_having | table | NULL | NULL | |
| gpu_groupby_exact_resident_topk | table | NULL | NULL | |
| gpu_groupby_exact_resident_where | table | NULL | NULL | |
| gpu_groupby_exact_resident_where_having | table | NULL | NULL | |
| gpu_groupby_exact_resident_where_topk | table | NULL | NULL | |
| gpu_groupby_sum_resident | table | NULL | NULL | |
| gpu_groupby_sum_resident_f64 | table | NULL | NULL | |
| gpu_groupby_sum_resident_f64_having | table | NULL | NULL | |
| gpu_groupby_sum_resident_f64_topk | table | NULL | NULL | |
| gpu_groupby_sum_resident_having | table | NULL | NULL | |
| gpu_groupby_sum_resident_topk | table | NULL | NULL | |
| gpu_inner_join | table | NULL | NULL | |
| gpu_invalidate | scalar | NULL | NULL | |
| gpu_join_count_resident | scalar | NULL | NULL | |
| gpu_join_materialize | scalar | NULL | NULL | |
| gpu_join_rows_resident | table | NULL | NULL | |
| gpu_join_sum_resident | scalar | NULL | NULL | |
| gpu_join_sum_resident_f64 | scalar | NULL | NULL | |
| gpu_last_stats | scalar | NULL | NULL | |
| gpu_left_join_count_resident | scalar | NULL | NULL | |
| gpu_left_join_sum_resident | scalar | NULL | NULL | |
| gpu_left_join_sum_resident_f64 | scalar | NULL | NULL | |
| gpu_max | aggregate | NULL | NULL | |
| gpu_max_resident | scalar | NULL | NULL | |
| gpu_min | aggregate | NULL | NULL | |
| gpu_min_resident | scalar | NULL | NULL | |
| gpu_note_rows | scalar | NULL | NULL | |
| gpu_prepare_resident | scalar | NULL | NULL | |
| gpu_resident_dict_component | scalar | NULL | NULL | |
| gpu_resident_dictionary | table | NULL | NULL | |
| gpu_resident_info | scalar | NULL | NULL | |
| gpu_residents | table | NULL | NULL | |
| gpu_rewrite_ast | scalar | NULL | NULL | |
| gpu_semi_join_count_resident | scalar | NULL | NULL | |
| gpu_semi_join_sum_resident | scalar | NULL | NULL | |
| gpu_semi_join_sum_resident_f64 | scalar | NULL | NULL | |
| gpu_store_columns | table | NULL | NULL | |
| gpu_sum | aggregate | NULL | NULL | |
| gpu_sum_resident | scalar | NULL | NULL | |
| gpu_sum_resident_f64 | scalar | NULL | NULL | |
| gpu_topk_resident | table | NULL | NULL | |
| gpu_topk_resident_f64 | table | NULL | NULL | |
| gpu_upload | aggregate | NULL | NULL | |
| gpu_upload_abort | scalar | NULL | NULL | |
| gpu_upload_begin | scalar | NULL | NULL | |
| gpu_upload_columns | aggregate | NULL | NULL | |
| gpu_upload_finish | scalar | NULL | NULL | |
| gpu_upload_pair | aggregate | NULL | NULL | |
| gpu_upload_pair_exact | aggregate | NULL | NULL | |
| gpu_upload_rows_exact | aggregate | NULL | NULL | |
| gpu_upload_status | scalar | NULL | NULL |
Overloaded Functions
This extension does not add any function overloads.
Added Types
This extension does not add any types.
Added Settings
This extension does not add any settings.