duckdb-gpudb 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. duckdb_gpudb-0.7.0/PKG-INFO +300 -0
  2. duckdb_gpudb-0.7.0/README.md +286 -0
  3. duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/PKG-INFO +300 -0
  4. duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/SOURCES.txt +31 -0
  5. duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/dependency_links.txt +1 -0
  6. duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/entry_points.txt +2 -0
  7. duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/requires.txt +1 -0
  8. duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/top_level.txt +1 -0
  9. duckdb_gpudb-0.7.0/gpudb/__init__.py +16 -0
  10. duckdb_gpudb-0.7.0/gpudb/__main__.py +7 -0
  11. duckdb_gpudb-0.7.0/gpudb/_aggs.py +126 -0
  12. duckdb_gpudb-0.7.0/gpudb/_classify.py +61 -0
  13. duckdb_gpudb-0.7.0/gpudb/_ctes.py +216 -0
  14. duckdb_gpudb-0.7.0/gpudb/_exprs.py +434 -0
  15. duckdb_gpudb-0.7.0/gpudb/_flatten.py +303 -0
  16. duckdb_gpudb-0.7.0/gpudb/_join.py +836 -0
  17. duckdb_gpudb-0.7.0/gpudb/_residency.py +2245 -0
  18. duckdb_gpudb-0.7.0/gpudb/_resolve.py +101 -0
  19. duckdb_gpudb-0.7.0/gpudb/_rewrite.py +1238 -0
  20. duckdb_gpudb-0.7.0/gpudb/_scope.py +133 -0
  21. duckdb_gpudb-0.7.0/gpudb/_shell.py +765 -0
  22. duckdb_gpudb-0.7.0/gpudb/_split.py +374 -0
  23. duckdb_gpudb-0.7.0/gpudb/_syntax.py +144 -0
  24. duckdb_gpudb-0.7.0/gpudb/_thresholds.py +509 -0
  25. duckdb_gpudb-0.7.0/gpudb/_views.py +133 -0
  26. duckdb_gpudb-0.7.0/gpudb/connection.py +3078 -0
  27. duckdb_gpudb-0.7.0/pyproject.toml +38 -0
  28. duckdb_gpudb-0.7.0/setup.cfg +32 -0
  29. duckdb_gpudb-0.7.0/setup.py +49 -0
  30. duckdb_gpudb-0.7.0/tests/test_residency_policy.py +1051 -0
  31. duckdb_gpudb-0.7.0/tests/test_shell.py +669 -0
  32. duckdb_gpudb-0.7.0/tests/test_wrapper.py +3367 -0
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: duckdb-gpudb
3
+ Version: 0.7.0
4
+ Summary: Plain DuckDB SQL on the GPU: a drop-in gpudb.connect() that answers statements on the device when that is faster, and lets DuckDB run everything else
5
+ Home-page: https://github.com/singhpratech/duckdbgpumetaldbram
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/singhpratech/duckdbgpumetaldbram
8
+ Project-URL: Documentation, https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/TRANSPARENT_DESIGN.md
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Database
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: duckdb>=1.4
14
+
15
+ # gpudb — plain DuckDB SQL on the GPU
16
+
17
+ **New in v0.7 — plain DuckDB SQL runs on the GPU.** No `gpu_*` calls, no query
18
+ changes: write the SQL you already write, and the GPU answers it when that is
19
+ measured faster — DuckDB answers everything else, with the same rows either
20
+ way. Apple Silicon Metal and NVIDIA CUDA, from one extension.
21
+
22
+ Ordinary DuckDB SQL, unchanged: statements the GPU answers faster are answered
23
+ on the device, everything else runs on DuckDB exactly as before, and the rows,
24
+ names and types are identical either way.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install duckdb-gpudb # the distribution is duckdb-gpudb; the import is gpudb
30
+ ```
31
+
32
+ There are **two pieces**: this package is the wrapper — the `gpudb` command and
33
+ `gpudb.connect()` — and the GPU code itself is a DuckDB extension. On **Apple
34
+ Silicon (macOS 15 or later)** and **x86-64 Linux (glibc 2.34 or newer, e.g.
35
+ Ubuntu 22.04 and later)** the wheel carries both: the v0.7.0 extension binary
36
+ travels inside the package, so that one line is the whole install. No `INSTALL`,
37
+ no build, no environment variable. One binary, and it has been run under both
38
+ DuckDB 1.4.5 and 1.5.5. The Linux wheel carries the CUDA-enabled build and its
39
+ own `libgomp.so.1`, so it needs nothing installed and reaches an NVIDIA GPU with
40
+ a driver R525 or newer, falling back to the CPU backend where there is none.
41
+
42
+ Anywhere else `pip` installs the pure-Python wheel and the extension comes from
43
+ DuckDB's own install:
44
+
45
+ ```sql
46
+ INSTALL gpudb FROM community; -- in any DuckDB >= 1.5.5 client
47
+ LOAD gpudb;
48
+ ```
49
+
50
+ The wrapper looks for the extension in this order: an explicit `extension=`
51
+ path, the `GPUDB_EXTENSION_PATH` environment variable, a `build-macos/` or
52
+ `build-linux/` directory next to a source checkout, the copy bundled in this
53
+ package, and finally the extension DuckDB itself has installed. A checkout's
54
+ own build comes before the bundled copy deliberately — someone who has just
55
+ built the extension is testing that binary. If it finds none — or finds one
56
+ older than this client — `con.extension_note` says so in one sentence and every
57
+ statement runs on DuckDB.
58
+
59
+ **Requires** Python >= 3.9 and the `duckdb` module >= 1.4. A bundled or
60
+ downloaded binary needs only DuckDB >= 1.2, because the loadable extension is
61
+ built against the stable C API v1.2.0; the registry builds gpudb separately for
62
+ each DuckDB version from 1.5.5 on. Apple silicon for the Metal backend, an
63
+ NVIDIA GPU for the CUDA one; plain SQL runs on the GPU by default on both, and
64
+ `GPUDB_CUDA_EXACT=0` turns the CUDA path off without a rebuild. On Linux the
65
+ extension needs `libgomp.so.1` at load time (`apt install libgomp1`), which the
66
+ wheel bundles and a registry install does not; a binary installed from the
67
+ community registry there reports `compiled=cpu` and carries no CUDA at all —
68
+ `SELECT gpu_build_info();` says which one you have.
69
+
70
+ ## The `gpudb` shell
71
+
72
+ The first way in, and the one that shows its work. This is one session on an
73
+ M4 Max over TPC-H SF1, opened read-only, on the v0.7.0 release build of
74
+ 2026-09-20 (the three `[gpudb] registered …` lines
75
+ are the extension announcing itself on stderr as DuckDB loads it):
76
+
77
+ ```
78
+ $ gpudb data/tpch_sf1/tpch.duckdb --readonly
79
+ [gpudb] registered gpu_inner_join + gpu_join_rows_resident
80
+ [gpudb] registered gpu_groupby_{sum,sum_f64,count,exact}_resident[_having|_topk] + gpu_topk_resident[_f64]
81
+ [gpudb] registered gpu_sum / gpu_min / gpu_max streaming aggregates + resident-column functions (gpu_upload, gpu_*_resident) (backend=Metal)
82
+ gpudb 0.7.0
83
+ backend: Metal · Apple M4 Max · 51.8 GiB device memory
84
+ transparent: available — every statement goes through the wrapper
85
+ database: data/tpch_sf1/tpch.duckdb
86
+ Enter .help for usage.
87
+
88
+ gpudb> SELECT l_partkey, sum(l_quantity) AS qty FROM lineitem GROUP BY l_partkey ORDER BY qty DESC LIMIT 5;
89
+ ┌───────────┬───────────────┐
90
+ │ l_partkey │ qty │
91
+ │ int64 │ decimal(38,2) │
92
+ ├───────────┼───────────────┤
93
+ │ 125009 │ 1642.00 │
94
+ │ 140633 │ 1562.00 │
95
+ │ 49981 │ 1553.00 │
96
+ │ 149443 │ 1537.00 │
97
+ │ 10426 │ 1513.00 │
98
+ └───────────┴───────────────┘
99
+
100
+ DuckDB (not_resident: the resident set is not ready yet) · 25.7 ms
101
+ ```
102
+
103
+ The first ask is on DuckDB on purpose: the columns are uploaded in short
104
+ row-id segments taken only while the connection is idle, so an upload never
105
+ runs a long scan beside a query you are waiting for. Ten seconds later, the
106
+ same statement, nine runs each way in the same session:
107
+
108
+ ```
109
+ gpudb> .gpu off
110
+ GPU path off — statements go straight to DuckDB.
111
+ … DuckDB · 30.4, 24.5, 16.5, 19.5, 20.9, 22.3, 21.6, 22.1, 23.0 ms
112
+ gpudb> .gpu on
113
+ GPU path on — residency: background.
114
+ … GPU (topk: the resident GROUP BY) · 12.9, 47.3, 10.0, 11.4, 14.1, 14.4, 14.4, 14.5, 14.8 ms
115
+ ```
116
+
117
+ Median 22.1 ms against 14.4 ms — 1.53×, with both series printed whole so the
118
+ warm-up runs and the wrapper's own measuring run stay visible. The nine GPU runs
119
+ spread from 10.0 to 14.8 ms with nothing changed between them: a statement this
120
+ short has more than one speed on Apple silicon depending on what else is waking,
121
+ which is why the wrapper measures in your process instead of trusting a
122
+ published ratio. Expect your own numbers rather than these.
123
+
124
+ The banner's `backend:` line names the runtime, the device as the driver
125
+ reports it, and the device memory the budget plans against; a build without a
126
+ GPU backend names no device, and `transparent:` says what is missing instead.
127
+
128
+ ### Options and dot-commands
129
+
130
+ ```bash
131
+ gpudb my.duckdb -c "SELECT …" # one statement
132
+ gpudb -f script.sql # a file (or: gpudb < script.sql)
133
+ python -m gpudb # the same entry point
134
+ ```
135
+
136
+ | Option | |
137
+ |---|---|
138
+ | `-c SQL` | run a statement and exit, repeatable |
139
+ | `-f FILE`, `--file FILE` | run a file and exit, repeatable; `-c` and `-f` run in the order given |
140
+ | `--readonly`, `--read-only` | open read-only |
141
+ | `--no-gpu` | every statement on DuckDB |
142
+ | `--residency background\|eager\|manual` | when tables become resident (default `background`) |
143
+ | `--memory-budget SIZE` | e.g. `16GB`, or `unlimited` |
144
+ | `--timer` / `--no-timer` | force the footer line on or off |
145
+ | `--debug` | show tracebacks instead of one-line errors |
146
+ | `--version` | the gpudb and duckdb versions |
147
+ | `-h`, `--help` | the same list, from the program |
148
+
149
+ A statement that fails ends a `-c` / `-f` / piped run with a non-zero exit
150
+ code; at the terminal the session keeps going. The footer is on at a terminal
151
+ and off in scripted output unless `--timer` says otherwise; colour follows
152
+ `NO_COLOR` and whether the output is a terminal.
153
+
154
+ | | |
155
+ |---|---|
156
+ | `.help` | the list |
157
+ | `.quit`, `.exit` | leave (Ctrl-D does too) |
158
+ | `.timer on\|off` | the footer line |
159
+ | `.gpu` | the last statement's whole `last_rewrite()`, `detail` included |
160
+ | `.gpu on\|off` | the transparent path, live |
161
+ | `.residents` | the resident sets, then the columns behind them with rows and width |
162
+ | `.memory` | the device-memory budget and what holds it |
163
+ | `.read FILE`, `.open [DATABASE]` | run a file, open another database |
164
+ | `.tables`, `.schema [TABLE]` | plain SQL underneath (`SHOW TABLES`, `DESCRIBE`) |
165
+ | `.version` | gpudb and duckdb versions |
166
+
167
+ Statements may span lines and end at `;`; Ctrl-C stops the running statement
168
+ (or clears what you were typing) and Ctrl-D leaves. History is kept in
169
+ `~/.gpudb_history` when the Python build has `readline`.
170
+
171
+ `.residents` prints two tables: the resident SETS — what a statement is waiting
172
+ on — and, under them, the COLUMNS those sets are views over. A lane is kept at
173
+ the narrowest signed width its values fit, so a column of small integers costs
174
+ one or two bytes a row rather than eight. Same session as above:
175
+
176
+ ```
177
+ gpudb> .residents
178
+ table columns state bytes estimated worth
179
+ main.lineitem l_partkey,l_quantity ready 80.1 MiB 161.7 MiB 3.99
180
+ 1 set · 80.1 MiB held · worth is ms saved per second per GiB · `.memory` for the budget
181
+
182
+ table column dtype rows width bytes state
183
+ lineitem l_partkey I64 6,001,215 4 B 68.7 MiB ready
184
+ lineitem l_quantity I64 6,001,215 2 B 11.4 MiB preparing
185
+ 2 resident columns · width is the bytes a row of the lane is stored at · `-` where the backend does not say
186
+ ```
187
+
188
+ `worth` is the DuckDB time a set saves per second of wall time, per GiB it
189
+ holds — what the memory budget compares when it has to choose. On a column
190
+ line, `state` means something narrower: whether *that lane* has a sort cache.
191
+ Only a key lane ever needs one, so a payload lane reads `preparing` and stays
192
+ there; the set's own state is what a statement waits on. Every dot-command and
193
+ option, and the session these came from, are in
194
+ [the shell guide](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_THE_SHELL.md).
195
+
196
+ There is a shell because the transparent path cannot live in the extension.
197
+ DuckDB's stable C extension API — the one the loadable extension uses on
198
+ purpose, so that one binary keeps working across DuckDB versions — has no hook
199
+ that sees a statement before it is planned. `LOAD gpudb` in the stock `duckdb`
200
+ CLI therefore gives the explicit `gpu_*` functions and nothing more. This shell
201
+ is a client that can look first: it splits statements with DuckDB's own
202
+ tokenizer, hands each one to `gpudb.connect()`, and prints the result with
203
+ DuckDB's own box renderer.
204
+
205
+ ## Python
206
+
207
+ ```python
208
+ import gpudb
209
+ con = gpudb.connect("my.duckdb") # same surface as duckdb.connect
210
+ con.execute("SELECT k, sum(v) FROM t GROUP BY k").fetchall()
211
+ con.last_rewrite() # what happened to that statement
212
+ ```
213
+
214
+ `connect()` takes `database`, `read_only` and `config` as `duckdb.connect`
215
+ does, plus `extension=` (an explicit path to the `.duckdb_extension`),
216
+ `transparent=` (default `True`), `residency=` (`background` | `eager` |
217
+ `manual`), `floor_rows=` (default 1,000,000), `idle_ms=` (default 20.0),
218
+ `thresholds=` (default `True`; `False` rewrites every exact shape regardless of
219
+ the predicted win — for parity testing only), `memory_budget=` (bytes or
220
+ `"16GB"`; `0` / `"unlimited"` removes the cap) and `log=` (a callable that
221
+ receives the wrapper's decisions as text).
222
+
223
+ Anything the wrapper does not define itself is delegated to the underlying
224
+ `duckdb.DuckDBPyConnection`. What it does define:
225
+
226
+ | | |
227
+ |---|---|
228
+ | `con.last_rewrite()` | `rewritten`, `reason`, `detail`, `form`, `tag`, `sql`, `statement`, `engine`, `round_trip_ms`, `fallback`, `error` |
229
+ | `con.memory()` | `budget`, `evictions`, `evictions_wasted`, `sets` |
230
+ | `con.residents()` | `{identity tag: state}` — `missing`, `pending`, `uploading`, `ready`, `stale`, `failed` |
231
+ | `con.store_columns()` | one dict per resident column: `table`, `column`, `dtype`, `rows`, `width`, `bytes`, `prepared` |
232
+ | `con.transparent` | readable and settable; `False` leaves every statement on DuckDB without dropping anything |
233
+ | `con.residency` | read-only: the mode passed to `connect()` |
234
+ | `con.cursor()` / `con.duplicate()` | another connection sharing the resident sets, with its own `last_rewrite()` |
235
+ | `con.interrupt()` | DuckDB's interrupt, on this connection's handle only |
236
+ | `con.extension_note` | empty while the loaded extension can serve the client; one sentence saying why not otherwise |
237
+
238
+ Give each thread its own `con.cursor()`: `last_rewrite()` is one record per
239
+ connection object, so two threads sharing one connection overwrite each other's.
240
+
241
+ ## Why a statement did not go to the GPU
242
+
243
+ The footer and `last_rewrite()["reason"]` carry a short code; `detail` is the
244
+ sentence behind it.
245
+
246
+ | Reason | What it means |
247
+ |---|---|
248
+ | `not_resident` | the columns are not on the device yet — the background uploader is working on it |
249
+ | `threshold` | a measured bound says DuckDB is faster for this shape and size, **or** this machine measured it slower and the template went back to DuckDB |
250
+ | `shape` | not a shape the rewrite expresses: a window function, `median`, `ROLLUP`, a set operation, a subquery in the select list |
251
+ | `double` | a `sum` / `avg` over `DOUBLE` or `FLOAT` — never rewritten, because native's own answer depends on the order the values are added |
252
+ | `ties` | a pushed `ORDER BY … LIMIT k` found two of the first *k* rows equal on the ordering value — which rows come back, and in what order, is DuckDB's to choose, and it answered the original. Decided against the data on every execution; a tie that keeps happening shows as `threshold` with the tie named in `detail`, until the 60-second re-measure. Through `sql()` — and so through the shell — the check runs on a side cursor inside the call, so the first statement of a data version asks the device once more and the verdict is remembered until the data changes |
253
+ | `backend` | this build has no GPU backend to rewrite for, or the installed extension is older than this client |
254
+ | `memory` | the set does not fit the device-memory budget; it is refused before the upload |
255
+ | `transaction` | a `BEGIN` is open, so the resident sets cannot be trusted |
256
+ | `params` | the statement takes prepared-statement parameters |
257
+ | `multi` | more than one statement in the call |
258
+ | `too_long` | the statement is longer than the 16 KB the wrapper parses |
259
+ | `error` | the rewritten statement raised and DuckDB answered the original — the text is in `error` |
260
+ | `off` / `manual` | the path is off (`.gpu off`, `--no-gpu`, `transparent=False`), or residency is `manual` and this set was not uploaded by hand |
261
+ | `nulls` `overflow` `decimal` `collation` `view` `temp` `ambiguous` `not_found` | a narrower refusal, each with its own sentence in `detail` |
262
+
263
+ What those look like in the shell's footer — these are the *shapes*, collected
264
+ from different statements and different sessions, not one run:
265
+
266
+ ```
267
+ GPU (plain: the resident GROUP BY) · 3.3 ms
268
+ GPU (topk: a key join materialised on the device) · 8.1 ms
269
+ DuckDB (threshold: 7 groups < 1000) · 5.0 ms
270
+ DuckDB (threshold: measured 4.20 ms rewritten vs 3.10 ms native (re-measured in 60 s)) · 3.2 ms
271
+ DuckDB (not_resident: the resident set is not ready yet) · 12.0 ms
272
+ DuckDB (ties: two of the first 5 rows tie on qty, so which rows come back — and in what order — is DuckDB's to choose, and DuckDB answered the original) · 49.9 ms
273
+ DuckDB (off: the transparent path is off on this connection) · 12.0 ms
274
+ ```
275
+
276
+ ## The third way in: explicit `gpu_*` functions
277
+
278
+ The shell and `gpudb.connect()` are two of three routes. The third needs no
279
+ wrapper at all: `INSTALL gpudb FROM community; LOAD gpudb;` in **any** DuckDB
280
+ client, in any language, and then call the functions by name —
281
+ `gpu_upload` / `gpu_upload_pair` to make a column resident once, then
282
+ `gpu_sum_resident`, `gpu_groupby_sum_resident` (with `_having` and `_topk`
283
+ forms that filter on the device), `gpu_topk_resident`, `gpu_inner_join` and the
284
+ exact `gpu_groupby_exact_*` family to read it back. `gpu_last_stats()` says
285
+ which processor ran and for how long. v0.7 registers 65 of these; v0.6.0
286
+ registered 38, and every one of those is still there unchanged.
287
+
288
+ It is full manual control, and it is the only route from the stock `duckdb`
289
+ CLI. The repository's README documents the whole surface.
290
+
291
+ ## Going deeper
292
+
293
+ - [The `gpudb` shell, end to end](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_THE_SHELL.md)
294
+ - [`gpudb.connect()`, every option](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_PYTHON.md)
295
+ - [Installing both pieces, platforms, troubleshooting](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/INSTALL.md)
296
+ - [How a plain `SELECT` reaches the GPU](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/TRANSPARENT_DESIGN.md) ·
297
+ [every environment variable](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/ENVIRONMENT.md)
298
+ - [Every measurement, losing cells included](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/BENCHMARK.md) ·
299
+ [every documented trade-off](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/KNOWN_ISSUES.md)
300
+ - [The repository](https://github.com/singhpratech/duckdbgpumetaldbram)
@@ -0,0 +1,286 @@
1
+ # gpudb — plain DuckDB SQL on the GPU
2
+
3
+ **New in v0.7 — plain DuckDB SQL runs on the GPU.** No `gpu_*` calls, no query
4
+ changes: write the SQL you already write, and the GPU answers it when that is
5
+ measured faster — DuckDB answers everything else, with the same rows either
6
+ way. Apple Silicon Metal and NVIDIA CUDA, from one extension.
7
+
8
+ Ordinary DuckDB SQL, unchanged: statements the GPU answers faster are answered
9
+ on the device, everything else runs on DuckDB exactly as before, and the rows,
10
+ names and types are identical either way.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install duckdb-gpudb # the distribution is duckdb-gpudb; the import is gpudb
16
+ ```
17
+
18
+ There are **two pieces**: this package is the wrapper — the `gpudb` command and
19
+ `gpudb.connect()` — and the GPU code itself is a DuckDB extension. On **Apple
20
+ Silicon (macOS 15 or later)** and **x86-64 Linux (glibc 2.34 or newer, e.g.
21
+ Ubuntu 22.04 and later)** the wheel carries both: the v0.7.0 extension binary
22
+ travels inside the package, so that one line is the whole install. No `INSTALL`,
23
+ no build, no environment variable. One binary, and it has been run under both
24
+ DuckDB 1.4.5 and 1.5.5. The Linux wheel carries the CUDA-enabled build and its
25
+ own `libgomp.so.1`, so it needs nothing installed and reaches an NVIDIA GPU with
26
+ a driver R525 or newer, falling back to the CPU backend where there is none.
27
+
28
+ Anywhere else `pip` installs the pure-Python wheel and the extension comes from
29
+ DuckDB's own install:
30
+
31
+ ```sql
32
+ INSTALL gpudb FROM community; -- in any DuckDB >= 1.5.5 client
33
+ LOAD gpudb;
34
+ ```
35
+
36
+ The wrapper looks for the extension in this order: an explicit `extension=`
37
+ path, the `GPUDB_EXTENSION_PATH` environment variable, a `build-macos/` or
38
+ `build-linux/` directory next to a source checkout, the copy bundled in this
39
+ package, and finally the extension DuckDB itself has installed. A checkout's
40
+ own build comes before the bundled copy deliberately — someone who has just
41
+ built the extension is testing that binary. If it finds none — or finds one
42
+ older than this client — `con.extension_note` says so in one sentence and every
43
+ statement runs on DuckDB.
44
+
45
+ **Requires** Python >= 3.9 and the `duckdb` module >= 1.4. A bundled or
46
+ downloaded binary needs only DuckDB >= 1.2, because the loadable extension is
47
+ built against the stable C API v1.2.0; the registry builds gpudb separately for
48
+ each DuckDB version from 1.5.5 on. Apple silicon for the Metal backend, an
49
+ NVIDIA GPU for the CUDA one; plain SQL runs on the GPU by default on both, and
50
+ `GPUDB_CUDA_EXACT=0` turns the CUDA path off without a rebuild. On Linux the
51
+ extension needs `libgomp.so.1` at load time (`apt install libgomp1`), which the
52
+ wheel bundles and a registry install does not; a binary installed from the
53
+ community registry there reports `compiled=cpu` and carries no CUDA at all —
54
+ `SELECT gpu_build_info();` says which one you have.
55
+
56
+ ## The `gpudb` shell
57
+
58
+ The first way in, and the one that shows its work. This is one session on an
59
+ M4 Max over TPC-H SF1, opened read-only, on the v0.7.0 release build of
60
+ 2026-09-20 (the three `[gpudb] registered …` lines
61
+ are the extension announcing itself on stderr as DuckDB loads it):
62
+
63
+ ```
64
+ $ gpudb data/tpch_sf1/tpch.duckdb --readonly
65
+ [gpudb] registered gpu_inner_join + gpu_join_rows_resident
66
+ [gpudb] registered gpu_groupby_{sum,sum_f64,count,exact}_resident[_having|_topk] + gpu_topk_resident[_f64]
67
+ [gpudb] registered gpu_sum / gpu_min / gpu_max streaming aggregates + resident-column functions (gpu_upload, gpu_*_resident) (backend=Metal)
68
+ gpudb 0.7.0
69
+ backend: Metal · Apple M4 Max · 51.8 GiB device memory
70
+ transparent: available — every statement goes through the wrapper
71
+ database: data/tpch_sf1/tpch.duckdb
72
+ Enter .help for usage.
73
+
74
+ gpudb> SELECT l_partkey, sum(l_quantity) AS qty FROM lineitem GROUP BY l_partkey ORDER BY qty DESC LIMIT 5;
75
+ ┌───────────┬───────────────┐
76
+ │ l_partkey │ qty │
77
+ │ int64 │ decimal(38,2) │
78
+ ├───────────┼───────────────┤
79
+ │ 125009 │ 1642.00 │
80
+ │ 140633 │ 1562.00 │
81
+ │ 49981 │ 1553.00 │
82
+ │ 149443 │ 1537.00 │
83
+ │ 10426 │ 1513.00 │
84
+ └───────────┴───────────────┘
85
+
86
+ DuckDB (not_resident: the resident set is not ready yet) · 25.7 ms
87
+ ```
88
+
89
+ The first ask is on DuckDB on purpose: the columns are uploaded in short
90
+ row-id segments taken only while the connection is idle, so an upload never
91
+ runs a long scan beside a query you are waiting for. Ten seconds later, the
92
+ same statement, nine runs each way in the same session:
93
+
94
+ ```
95
+ gpudb> .gpu off
96
+ GPU path off — statements go straight to DuckDB.
97
+ … DuckDB · 30.4, 24.5, 16.5, 19.5, 20.9, 22.3, 21.6, 22.1, 23.0 ms
98
+ gpudb> .gpu on
99
+ GPU path on — residency: background.
100
+ … GPU (topk: the resident GROUP BY) · 12.9, 47.3, 10.0, 11.4, 14.1, 14.4, 14.4, 14.5, 14.8 ms
101
+ ```
102
+
103
+ Median 22.1 ms against 14.4 ms — 1.53×, with both series printed whole so the
104
+ warm-up runs and the wrapper's own measuring run stay visible. The nine GPU runs
105
+ spread from 10.0 to 14.8 ms with nothing changed between them: a statement this
106
+ short has more than one speed on Apple silicon depending on what else is waking,
107
+ which is why the wrapper measures in your process instead of trusting a
108
+ published ratio. Expect your own numbers rather than these.
109
+
110
+ The banner's `backend:` line names the runtime, the device as the driver
111
+ reports it, and the device memory the budget plans against; a build without a
112
+ GPU backend names no device, and `transparent:` says what is missing instead.
113
+
114
+ ### Options and dot-commands
115
+
116
+ ```bash
117
+ gpudb my.duckdb -c "SELECT …" # one statement
118
+ gpudb -f script.sql # a file (or: gpudb < script.sql)
119
+ python -m gpudb # the same entry point
120
+ ```
121
+
122
+ | Option | |
123
+ |---|---|
124
+ | `-c SQL` | run a statement and exit, repeatable |
125
+ | `-f FILE`, `--file FILE` | run a file and exit, repeatable; `-c` and `-f` run in the order given |
126
+ | `--readonly`, `--read-only` | open read-only |
127
+ | `--no-gpu` | every statement on DuckDB |
128
+ | `--residency background\|eager\|manual` | when tables become resident (default `background`) |
129
+ | `--memory-budget SIZE` | e.g. `16GB`, or `unlimited` |
130
+ | `--timer` / `--no-timer` | force the footer line on or off |
131
+ | `--debug` | show tracebacks instead of one-line errors |
132
+ | `--version` | the gpudb and duckdb versions |
133
+ | `-h`, `--help` | the same list, from the program |
134
+
135
+ A statement that fails ends a `-c` / `-f` / piped run with a non-zero exit
136
+ code; at the terminal the session keeps going. The footer is on at a terminal
137
+ and off in scripted output unless `--timer` says otherwise; colour follows
138
+ `NO_COLOR` and whether the output is a terminal.
139
+
140
+ | | |
141
+ |---|---|
142
+ | `.help` | the list |
143
+ | `.quit`, `.exit` | leave (Ctrl-D does too) |
144
+ | `.timer on\|off` | the footer line |
145
+ | `.gpu` | the last statement's whole `last_rewrite()`, `detail` included |
146
+ | `.gpu on\|off` | the transparent path, live |
147
+ | `.residents` | the resident sets, then the columns behind them with rows and width |
148
+ | `.memory` | the device-memory budget and what holds it |
149
+ | `.read FILE`, `.open [DATABASE]` | run a file, open another database |
150
+ | `.tables`, `.schema [TABLE]` | plain SQL underneath (`SHOW TABLES`, `DESCRIBE`) |
151
+ | `.version` | gpudb and duckdb versions |
152
+
153
+ Statements may span lines and end at `;`; Ctrl-C stops the running statement
154
+ (or clears what you were typing) and Ctrl-D leaves. History is kept in
155
+ `~/.gpudb_history` when the Python build has `readline`.
156
+
157
+ `.residents` prints two tables: the resident SETS — what a statement is waiting
158
+ on — and, under them, the COLUMNS those sets are views over. A lane is kept at
159
+ the narrowest signed width its values fit, so a column of small integers costs
160
+ one or two bytes a row rather than eight. Same session as above:
161
+
162
+ ```
163
+ gpudb> .residents
164
+ table columns state bytes estimated worth
165
+ main.lineitem l_partkey,l_quantity ready 80.1 MiB 161.7 MiB 3.99
166
+ 1 set · 80.1 MiB held · worth is ms saved per second per GiB · `.memory` for the budget
167
+
168
+ table column dtype rows width bytes state
169
+ lineitem l_partkey I64 6,001,215 4 B 68.7 MiB ready
170
+ lineitem l_quantity I64 6,001,215 2 B 11.4 MiB preparing
171
+ 2 resident columns · width is the bytes a row of the lane is stored at · `-` where the backend does not say
172
+ ```
173
+
174
+ `worth` is the DuckDB time a set saves per second of wall time, per GiB it
175
+ holds — what the memory budget compares when it has to choose. On a column
176
+ line, `state` means something narrower: whether *that lane* has a sort cache.
177
+ Only a key lane ever needs one, so a payload lane reads `preparing` and stays
178
+ there; the set's own state is what a statement waits on. Every dot-command and
179
+ option, and the session these came from, are in
180
+ [the shell guide](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_THE_SHELL.md).
181
+
182
+ There is a shell because the transparent path cannot live in the extension.
183
+ DuckDB's stable C extension API — the one the loadable extension uses on
184
+ purpose, so that one binary keeps working across DuckDB versions — has no hook
185
+ that sees a statement before it is planned. `LOAD gpudb` in the stock `duckdb`
186
+ CLI therefore gives the explicit `gpu_*` functions and nothing more. This shell
187
+ is a client that can look first: it splits statements with DuckDB's own
188
+ tokenizer, hands each one to `gpudb.connect()`, and prints the result with
189
+ DuckDB's own box renderer.
190
+
191
+ ## Python
192
+
193
+ ```python
194
+ import gpudb
195
+ con = gpudb.connect("my.duckdb") # same surface as duckdb.connect
196
+ con.execute("SELECT k, sum(v) FROM t GROUP BY k").fetchall()
197
+ con.last_rewrite() # what happened to that statement
198
+ ```
199
+
200
+ `connect()` takes `database`, `read_only` and `config` as `duckdb.connect`
201
+ does, plus `extension=` (an explicit path to the `.duckdb_extension`),
202
+ `transparent=` (default `True`), `residency=` (`background` | `eager` |
203
+ `manual`), `floor_rows=` (default 1,000,000), `idle_ms=` (default 20.0),
204
+ `thresholds=` (default `True`; `False` rewrites every exact shape regardless of
205
+ the predicted win — for parity testing only), `memory_budget=` (bytes or
206
+ `"16GB"`; `0` / `"unlimited"` removes the cap) and `log=` (a callable that
207
+ receives the wrapper's decisions as text).
208
+
209
+ Anything the wrapper does not define itself is delegated to the underlying
210
+ `duckdb.DuckDBPyConnection`. What it does define:
211
+
212
+ | | |
213
+ |---|---|
214
+ | `con.last_rewrite()` | `rewritten`, `reason`, `detail`, `form`, `tag`, `sql`, `statement`, `engine`, `round_trip_ms`, `fallback`, `error` |
215
+ | `con.memory()` | `budget`, `evictions`, `evictions_wasted`, `sets` |
216
+ | `con.residents()` | `{identity tag: state}` — `missing`, `pending`, `uploading`, `ready`, `stale`, `failed` |
217
+ | `con.store_columns()` | one dict per resident column: `table`, `column`, `dtype`, `rows`, `width`, `bytes`, `prepared` |
218
+ | `con.transparent` | readable and settable; `False` leaves every statement on DuckDB without dropping anything |
219
+ | `con.residency` | read-only: the mode passed to `connect()` |
220
+ | `con.cursor()` / `con.duplicate()` | another connection sharing the resident sets, with its own `last_rewrite()` |
221
+ | `con.interrupt()` | DuckDB's interrupt, on this connection's handle only |
222
+ | `con.extension_note` | empty while the loaded extension can serve the client; one sentence saying why not otherwise |
223
+
224
+ Give each thread its own `con.cursor()`: `last_rewrite()` is one record per
225
+ connection object, so two threads sharing one connection overwrite each other's.
226
+
227
+ ## Why a statement did not go to the GPU
228
+
229
+ The footer and `last_rewrite()["reason"]` carry a short code; `detail` is the
230
+ sentence behind it.
231
+
232
+ | Reason | What it means |
233
+ |---|---|
234
+ | `not_resident` | the columns are not on the device yet — the background uploader is working on it |
235
+ | `threshold` | a measured bound says DuckDB is faster for this shape and size, **or** this machine measured it slower and the template went back to DuckDB |
236
+ | `shape` | not a shape the rewrite expresses: a window function, `median`, `ROLLUP`, a set operation, a subquery in the select list |
237
+ | `double` | a `sum` / `avg` over `DOUBLE` or `FLOAT` — never rewritten, because native's own answer depends on the order the values are added |
238
+ | `ties` | a pushed `ORDER BY … LIMIT k` found two of the first *k* rows equal on the ordering value — which rows come back, and in what order, is DuckDB's to choose, and it answered the original. Decided against the data on every execution; a tie that keeps happening shows as `threshold` with the tie named in `detail`, until the 60-second re-measure. Through `sql()` — and so through the shell — the check runs on a side cursor inside the call, so the first statement of a data version asks the device once more and the verdict is remembered until the data changes |
239
+ | `backend` | this build has no GPU backend to rewrite for, or the installed extension is older than this client |
240
+ | `memory` | the set does not fit the device-memory budget; it is refused before the upload |
241
+ | `transaction` | a `BEGIN` is open, so the resident sets cannot be trusted |
242
+ | `params` | the statement takes prepared-statement parameters |
243
+ | `multi` | more than one statement in the call |
244
+ | `too_long` | the statement is longer than the 16 KB the wrapper parses |
245
+ | `error` | the rewritten statement raised and DuckDB answered the original — the text is in `error` |
246
+ | `off` / `manual` | the path is off (`.gpu off`, `--no-gpu`, `transparent=False`), or residency is `manual` and this set was not uploaded by hand |
247
+ | `nulls` `overflow` `decimal` `collation` `view` `temp` `ambiguous` `not_found` | a narrower refusal, each with its own sentence in `detail` |
248
+
249
+ What those look like in the shell's footer — these are the *shapes*, collected
250
+ from different statements and different sessions, not one run:
251
+
252
+ ```
253
+ GPU (plain: the resident GROUP BY) · 3.3 ms
254
+ GPU (topk: a key join materialised on the device) · 8.1 ms
255
+ DuckDB (threshold: 7 groups < 1000) · 5.0 ms
256
+ DuckDB (threshold: measured 4.20 ms rewritten vs 3.10 ms native (re-measured in 60 s)) · 3.2 ms
257
+ DuckDB (not_resident: the resident set is not ready yet) · 12.0 ms
258
+ DuckDB (ties: two of the first 5 rows tie on qty, so which rows come back — and in what order — is DuckDB's to choose, and DuckDB answered the original) · 49.9 ms
259
+ DuckDB (off: the transparent path is off on this connection) · 12.0 ms
260
+ ```
261
+
262
+ ## The third way in: explicit `gpu_*` functions
263
+
264
+ The shell and `gpudb.connect()` are two of three routes. The third needs no
265
+ wrapper at all: `INSTALL gpudb FROM community; LOAD gpudb;` in **any** DuckDB
266
+ client, in any language, and then call the functions by name —
267
+ `gpu_upload` / `gpu_upload_pair` to make a column resident once, then
268
+ `gpu_sum_resident`, `gpu_groupby_sum_resident` (with `_having` and `_topk`
269
+ forms that filter on the device), `gpu_topk_resident`, `gpu_inner_join` and the
270
+ exact `gpu_groupby_exact_*` family to read it back. `gpu_last_stats()` says
271
+ which processor ran and for how long. v0.7 registers 65 of these; v0.6.0
272
+ registered 38, and every one of those is still there unchanged.
273
+
274
+ It is full manual control, and it is the only route from the stock `duckdb`
275
+ CLI. The repository's README documents the whole surface.
276
+
277
+ ## Going deeper
278
+
279
+ - [The `gpudb` shell, end to end](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_THE_SHELL.md)
280
+ - [`gpudb.connect()`, every option](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/USING_PYTHON.md)
281
+ - [Installing both pieces, platforms, troubleshooting](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/INSTALL.md)
282
+ - [How a plain `SELECT` reaches the GPU](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/TRANSPARENT_DESIGN.md) ·
283
+ [every environment variable](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/docs/ENVIRONMENT.md)
284
+ - [Every measurement, losing cells included](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/BENCHMARK.md) ·
285
+ [every documented trade-off](https://github.com/singhpratech/duckdbgpumetaldbram/blob/main/KNOWN_ISSUES.md)
286
+ - [The repository](https://github.com/singhpratech/duckdbgpumetaldbram)