Repository navigation
[Proposal] Exploring vectorized execution for Cloudberry as a pure extension (vec_executor) #2267
Replies: 4 comments
|
@leborchuk @Alena0704 @igor-suhorukov Would appreciate your thoughts on this when you have time. @igor-suhorukov, pg_vexec (#2065) takes a different route to the same goal; I'd be glad to compare notes. |
|
Excellent proposal. We can advance this work through community-driven efforts. We need more thoughts on it. |
|
Thank you, all looks promising! Yes, I keep coming up with the idea of writing a vectorized engine, but I don't dare to start work here. Thank you for sharing the result, it's very interesting, and I think something really interesting can be done here. What kind of approach has developed in my mind. I may be wrong, because I haven't started experimenting or writing code yet. We have a seq scan that can return data in arrow format - iceberg and pax. Then, instead of immediately converting them to PG format, we leave them in arrow and use vector operations to perform further transformations. We do this until we encounter an unsupported operation or the second statement does not contain data in PG format. Visually, we sort of start performing vector-style operations from the very bottom of the execution plan and gradually move up, leaving the data in arrow format for as long as possible. volcano - style is not a problem here, because it's more about transferring control between nodes. Yes, we start with root and move recursively through branches to get results, but this does not prevent Clickhouse from building a very efficient engine. I really like velox. I would like to try this engine. Rust scares me a lot, to be honest. How I would like to use it is to learn how to perform individual execution plan operations using velox. First of all, these are hash join, group by, and data transfer in arrow format between slices. Replacing not individual slices, but individual operations. Because I want more control over the engine and I want to be able to add my own operations or improve the implementation of the engine to meet the requirements of Cloudberry. |
|
I support this proposal. I believe this feature could help drive Cloudberry adoption by addressing a practical customer need. It would also help close a potential gap with Apache Doris, so customers evaluating both products would not see a significant disadvantage in this area. |
Uh oh!
There was an error while loading. Please reload this page.
Proposers
@MisterRaindrop
Proposal Status
Under Discussion
Abstract
This is a personal exploration of one question: how far can Cloudberry get with vectorized execution if it is added only as an extension, with no change to the kernel?
I built
vec_executor, an extension that hands the slices of a query plan it can run to a vectorized engine and leaves the rest to Cloudberry's executor, so every query still runs and returns the same rows. The engine is currently based on Apache DataFusion and plugs in through existing hooks.First results are encouraging: on a single host, TPC-H at scale factor 100 runs much faster than on the same cluster without the extension, with identical results. These are basic tests only; tests with more data, more nodes and more detail are being prepared.
I am posting this to share the exploration and to hear the community's views: whether the direction is worth pursuing, what concerns people most, and where the kernel could help. The code is open source under the Apache License 2.0: https://github.com/MisterRaindrop/cloudberry-datafusion
Motivation
Cloudberry's executor follows PostgreSQL's volcano model and processes one row at a time. For analytical queries dominated by scans, aggregates and joins, much of the CPU time goes to per-row function calls, interpreted expressions, and forming and deforming tuples. Cloudberry already has a columnar storage format, PAX, but the executor still works row by row, so the benefit of columnar storage shows up mainly in I/O.
A vectorized engine processes batches of columns at a time (here in Arrow format), which removes much of the per-row overhead and can run on several threads within a segment.
Rewriting the executor inside the kernel is costly and slow. This exploration asks whether Cloudberry can use a mature vectorized engine through an extension, keeping Cloudberry's semantics and operational behaviour, and if so, where the limits of that approach lie.
Related work. #2065 describes pg_vexec, a vectorized executor built as an extension on a PostgreSQL 19 port of Cloudberry.
vec_executordiffers in two ways: it targets the current Cloudberrymainas it is, without any kernel patch, and it delegates whole plan slices to a separate engine library behind a C interface rather than implementing vectorized nodes inside PostgreSQL. I would be glad to compare notes.Implementation
Design principles
planner_hook,ExecutorStart_hook,ExecutorRun_hookandExplainOneQuery_hook. Without the extension loaded, or withvec.mode = off(the default), Cloudberry behaves exactly as before.vec.motion_batches, experimental).Architecture
The extension has two layers:
src/, C) runs in every backend and is the only part that calls PostgreSQL. It checks whether each slice can run in the engine, translates the slices that can into a plan IR (JSON), scans tables with the query's snapshot, sends and receives Motion data on the interconnect, leases the engine's memory from the vmem tracker, and turns engine errors intoereports.engines/<name>/) is a separate library,vec_engine_<name>.so, chosen byvec.engineand loaded on first use. It runs a slice's plan on its own threads.engines/datafusionis written in Rust on Apache DataFusion;engines/nullis written in C, runs nothing, and is used for tests.Three written contracts sit between them, so that a new engine can be built from them alone:
include/vec_engine.h: the function table, the rules for threads, signals, errors and memory, and the capabilities an engine declares (VecCaps).docs/ir.md: the plan IR: nodes, expressions, types, and the PostgreSQL functions an engine must compute with PostgreSQL's semantics.docs/wire.md: the Arrow IPC streams between engine slices, including cdbhash routing, on which both ends must agree byte for byte.Data passes between the host and the engine through the Arrow C Data Interface. Because the engine is replaceable, DataFusion is not a fixed choice; a C++ engine such as Velox could be added later.
For PAX tables there is an optional direct reader that reads PAX files column by column on the engine's threads, without forming tuples. It is built by patching a copy of the PAX sources; neither the source tree nor the installed
pax.sois changed.Current status
What runs in the engine:
NOT IN), which spill; merge joins, run as hash joins; nested loop joins.DISTINCTaggregates, grouping sets, and the common aggregate functions.LIMIT, window functions,UNION ALL/INTERSECT/EXCEPT, and correlated aggregate subqueries.numeric,bool, date and time types,text/varchar/char(n), with PostgreSQL's semantics, including the same errors and SQLSTATEs, the same numeric scales and rounding, and short-circuitAND/OR.A slice runs in the engine only when everything in it can; otherwise the whole slice stays on PostgreSQL, and
EXPLAINsays why.Coverage (TPC-H and TPC-DS at SF1, both planners):
Tests: 24 regression test files, each query run with the extension on and off and the results compared; and an engine-level check that runs the same tests once with the DataFusion engine and once with the null engine and requires the same results.
Size, without comments, blank lines and tests: about 14,500 lines of C in the host and about 12,400 lines of Rust in the DataFusion engine.
Preliminary results
These are basic tests to check whether the approach is viable, not a full performance evaluation.
TPC-H SF100: one host, 3 primaries, PAX tables, compared with the same cluster without the extension. Each query ran 5 times; the table gives the sum of the 22 medians, warm cache.
docs/benchmarks.md.The bottleneck is now on the storage side. Profiling shows that most of the CPU time of scan-heavy queries goes into reading PAX data: about half for Q1 and about three quarters for Q19. That time is spent mainly on zstd decompression and on converting PAX's variable-length PostgreSQL
numericvalues into fixed-width decimals. Further work on the execution side gains little. A storage layer that offered a format and interface suited to vectorized reads (for example fixed-width numbers, lighter compression, and an API for reading columns in batches) would allow clear further gains. This concerns PAX itself, is beyond what an extension can do, and is one of the topics I would like to discuss.Resource management and operations
To check that the engine stays under Cloudberry's control, I ran these basic tests on a single host:
cpu_max_percent=10(2 cores) a query used exactly 2.0 cores, and its time went from 1.9 s to 7.3 s.memory_quotasets the engine's memory budget: a large quota avoids spilling, a small one spills, as without the extension. All of the engine's memory is counted in vmem. At the vmem limit, Cloudberry's runaway cleaner cancels queries with a clear error and nothing crashes.statement_timeout, terminating the connection, killing a QE, and killing the coordinator's backend, the engine stops within 0.1 to 0.4 s; threads, temporary files and CPU are released, and the cluster stays available.vec.modeis off by default and can be turned on per session or per database. When off, plans are neither rewritten nor executed by the engine.Known differences and limits
I compared results with the extension on and off for about 200 queries on heap and PAX tables, under both planners. The problems this found have been fixed; the remaining differences are listed in the README. The main ones:
sum/avgcan differ in the last digits, because values are added in a different order.LIMITand noORDER BY, the engine may evaluate rows past the limit, so a query can fail where PostgreSQL does not.WHEREcondition that can fail may be skipped on rows a cheaper condition of the sameWHEREalready rejects, so a query that fails on PostgreSQL may return rows.EXPLAIN ANALYZEcounts rows for a slice's top node only.Limits:
gp_workfile_limit_per_queryset, a native query over the limit fails, but the engine's spilling is not limited. This is the most visible operational gap today.Rollout/Adoption Plan
The extension is built and installed separately and is off by default, so it does not affect existing users.
Next steps on my side:
Questions for the community:
gp_toolkitviews?Links:
docs/benchmarks.mddocs/internals.mddocs/engines.md,docs/ir.md,docs/wire.mdAre you willing to submit a PR?
All reactions