alitrack

duckdb-luajit 接上 GPU:SQL 里算 SVD 快了 18 倍

数据分析绕不开矩阵运算。

PCA、线性回归、协同过滤、因子分析——转一圈下来,最后都是 SVD、特征分解这类 LAPACK 算子在干活。DuckDB 的 SQL 很快,但真要算矩阵,只能把数据搬出去,用 Python 算完再搬回来。

更麻烦的是,DuckDB 官方对 GPU 的态度一直是「不需要」。Hannes Mühleisen 公开说过 "no funky GPU drivers",2026 年 5 月揭晓的 "Next Big Thing" 也是 Quack 协议,不是 GPU。

但我还是把矩阵计算搬上了 GPU。600 万行真实数据,SVD 快了 18.6 倍。

● ● ●

GPU 数据库坟场

先把话说清楚:GPU 数据库不是新东西,坟头草已经老高了。

全引擎接管路线——把整个查询引擎搬到 GPU——这些年折了一大片,还在活跃的只剩 Sirius 一个研究项目。DuckDB 生态里我也翻过一遍:注册表 308 个扩展,跟 GPU 相关的只有 1 个(gpudb,三个聚合函数,pre-alpha)。

为什么没人做?因为有人试过。oluies 在 Apple Silicon 上把 DuckDB 搬到 GPU 的实验是个很好的反面教材:把关系算子(scan、join 这类)搬上 GPU,反而慢 12 倍。传输开销吃掉了所有收益,CPU 的顺序扫描太强了。

但注意这个实验的另一个数字:数值层常驻 GPU 有 5.5× 加速。关系算子不该上 GPU,矩阵代数该上。这个区分就是机会。

● ● ●

零 C 桥接上 cuBLAS

duckdb-luajit 是给 DuckDB 装 Lua 函数的扩展,之前已经用 LuaJIT FFI 直接调过 HiGHS 求解器和 OpenBLAS。GPU 只是又一个后端——cuBLAS、cuSOLVER 都是 C API,LuaJIT FFI 可以直接调,一行 C 桥代码都不用写。

linalg 库现在有 7 个算子走 GPU:matmul、svd、eigh、inv、lu、chol、qr。后端选择逻辑就一小段:

local backend = p.backend or 'auto'
if backend ~= 'cpu' and gpu and gpu_ops[op] then
  -- GPU 路径(cuBLAS / cuSOLVER)
end
-- 否则 CPU(OpenBLAS)

无 GPU 的机器静默走 CPU,行为不变。SwanFlow 画布上的 linalg 节点也能选 backend,可视化流程里直接指定 gpu/cpu/auto。

linalg GPU 架构

linalg GPU 架构

● ● ●

数字

裸性能先看一眼:SGEMM 2048³ 跑到 53.4 TFLOPS(FP32)。4090 的 FP32 峰值 82.6 TFLOPS,利用率 65%,对 FFI 直调来说已经不错。FP64 是这张卡的弱项,DGEMM 2048³ 只有 1.13 TFLOPS——但 4090 的 FP64 峰值本来就只有 1.3T,这已经是 87% 了。

真正的考验是端到端。用 TPC-H lineitem 表 600 万行真实数据(SF=1),取 4 个数值列做标准化,算相关矩阵 XᵀX 和 SVD:

步骤
CPU (OpenBLAS)
GPU resident
加速
XᵀX(dgemm)
17.6 ms
15.7 ms
1.1×
SVD(Xgesvd)
1156 ms
62 ms
18.6×
合计
1174 ms
87.7 ms
13.4×

SVD 是主加速源,XᵀX 这种带宽型算子几乎没差别——这符合预期,也验证了「数值层上 GPU、关系层别碰」的判断。

数值正确性也过了:QR 重建 A=Q·R 的 maxerr=0,Cholesky 重建误差 8.9e-16,逆矩阵 A·A⁻¹=I 误差 2.2e-16,特征分解 A·v=λv 误差 5.6e-16,奇异值与 CPU 逐位一致。

SQL 里全链路也通了。观测表 6×3 → SQL 聚合出 XᵀX → 一行调用:

SELECT luajit_s('linalg', '{"op":"svd","a":[...],"m":6,"n":6}') AS res;

GPU SVD 给出 s=[14.365, 13.110, 3.955],GPU eigh 给出 w=[3.955, 13.110, 14.365]——对称阵的奇异值必须等于特征值,σ=λ 交叉验证通过。不是「能跑」,是算得对。

● ● ●

坑

几个值得记的坑。

32 位 dgesvd 在 Ada 4090 上 N≥768 必崩。cuSOLVER 的经典 32 位 API 在新卡上有 bug,st=6 EXECUTION_FAILED,必须用 64 位通用 API Xgesvd(PyTorch 的 SVD 也是走这条路)。这是第一条经验:GPU 上线性代数一律用 X 系列通用 API。

传输比计算贵。16MiB 数据 H2D+D2H 往返 2.25ms,小矩阵搬上搬下纯亏。所以默认策略是小矩阵走 CPU,大矩阵走 GPU;多次算子时用 resident 模式,一次 H2D,连续算完再 D2H。600 万行的 XᵀX 是 192MB,H2D 只要 9.9ms(19.4 GB/s)。

冷启动 30ms。第一次调 cuBLAS 有初始化开销,模块级缓存 handle 解决——这也意味着生产环境要长生命周期连接,别每个 SQL 都新建。

LuaJIT FFI 有个 vararg 参数错位 bug。f(..., x, y) 尾部追加参数时,第 3 个参数会变成 uint64_t[1],workspace 查询这种可变参数调用必须内联精确参数列表。排查了很久,最后是最小复现脚本定位的。

● ● ●

边界

诚实说几点。

关系算子别上 GPU——12× 慢的实验是真的,DuckDB 的 CPU 顺序扫描太强,传输开销吃光收益。

小矩阵别上 GPU——4×4 的矩阵,CPU 微秒级,GPU 光传输就 2ms。linalg 的自动阈值会拦下。

FP64 是 4090 的弱项(1:64 的比例)。单精度才是这张卡的甜点,TF32 更好。真要双精度大矩阵,那是 A100/H100 的活。

● ● ●

下一步

Mac 上的 MLX C API 是下一个目标——M3 Ultra 的共享内存架构,对矩阵运算来说比 PCIe 传输友好得多。等这个通了,linalg 的 GPU 后端就有第二个生产级目标平台。

代码在 github.com/alitrack/duckdb-luajit-libs(MIT),linalg 的 GPU 后端已经合入工作副本。想试:加载扩展后设 LUA_LINALG_GPU=1,或者直接给参数加 backend='gpu'。

参考来源:

  • ●
    Sirius(GPU 数据库全引擎接管路线):https://github.com/sirius-db/sirius
  • ●
    gpudb(DuckDB 社区 GPU 扩展):https://github.com/singhpratech/duckdbgpumetaldbram
  • ●
    oluies 的 DuckDB on Apple Silicon GPU 实验(关系算子 12× 慢):https://github.com/oluies/duckdbmacsillicongpu-
  • ●
    DuckDB 社区扩展注册表:https://community-extensions.duckdb.org/
  • ●
    duckdb-luajit-libs:https://github.com/alitrack/duckdb-luajit-libs