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.
- duckdb_gpudb-0.7.0/PKG-INFO +300 -0
- duckdb_gpudb-0.7.0/README.md +286 -0
- duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/PKG-INFO +300 -0
- duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/SOURCES.txt +31 -0
- duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/dependency_links.txt +1 -0
- duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/entry_points.txt +2 -0
- duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/requires.txt +1 -0
- duckdb_gpudb-0.7.0/duckdb_gpudb.egg-info/top_level.txt +1 -0
- duckdb_gpudb-0.7.0/gpudb/__init__.py +16 -0
- duckdb_gpudb-0.7.0/gpudb/__main__.py +7 -0
- duckdb_gpudb-0.7.0/gpudb/_aggs.py +126 -0
- duckdb_gpudb-0.7.0/gpudb/_classify.py +61 -0
- duckdb_gpudb-0.7.0/gpudb/_ctes.py +216 -0
- duckdb_gpudb-0.7.0/gpudb/_exprs.py +434 -0
- duckdb_gpudb-0.7.0/gpudb/_flatten.py +303 -0
- duckdb_gpudb-0.7.0/gpudb/_join.py +836 -0
- duckdb_gpudb-0.7.0/gpudb/_residency.py +2245 -0
- duckdb_gpudb-0.7.0/gpudb/_resolve.py +101 -0
- duckdb_gpudb-0.7.0/gpudb/_rewrite.py +1238 -0
- duckdb_gpudb-0.7.0/gpudb/_scope.py +133 -0
- duckdb_gpudb-0.7.0/gpudb/_shell.py +765 -0
- duckdb_gpudb-0.7.0/gpudb/_split.py +374 -0
- duckdb_gpudb-0.7.0/gpudb/_syntax.py +144 -0
- duckdb_gpudb-0.7.0/gpudb/_thresholds.py +509 -0
- duckdb_gpudb-0.7.0/gpudb/_views.py +133 -0
- duckdb_gpudb-0.7.0/gpudb/connection.py +3078 -0
- duckdb_gpudb-0.7.0/pyproject.toml +38 -0
- duckdb_gpudb-0.7.0/setup.cfg +32 -0
- duckdb_gpudb-0.7.0/setup.py +49 -0
- duckdb_gpudb-0.7.0/tests/test_residency_policy.py +1051 -0
- duckdb_gpudb-0.7.0/tests/test_shell.py +669 -0
- 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)
|