Search Shortcut cmd + k | ctrl + k

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.

Maintainer(s): singhpratech

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:

  1. 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.
  2. Never a different answer. Integer and DECIMAL aggregates 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.