[VL] Fix empty GPU broadcast joins and CudfVector host read crashes in the cuDF backend - #12471
[VL] Fix empty GPU broadcast joins and CudfVector host read crashes in the cuDF backend#12471ReemaAlzaid wants to merge 19 commits into
Conversation
Add materializeVeloxRowVector() (cpp/velox/utils/CudfVectorUtils.h) and call it at every GPU->CPU boundary that reads RowVector children on the host: CudfVectorStream/RowVectorStream::next, VeloxColumnarBatch ensureFlattened/compose/select/toUnsafeRow, VeloxBatchResizer, JniHashTable nativeHashTableBuild, VeloxJniWrapper prune, VeloxColumnarBatchSerializer append/framedSerializeWithStats, HashTableBuilder::addInput. Eliminates the broadcast/shuffle childAt crash on GPU-resident CudfVector (0 host children). Also wires cudf.allowCpuFallback through VeloxBackend/GlutenConfig.
VeloxColumnarToRowConverter::convert read the raw (device-resident) CudfVector via getRowVector(); switch to getFlattenedRowVector() so it materializes to host first. This is the one host-read site the materialize set missed. It feeds both the broadcast build-side relation (UnsafeColumnarBuildSideRelation columnar->row) and result return, so the gap caused empty broadcast joins and VARCHAR-offset SIGSEGVs (e.g. q15).
For offloaded broadcast hash joins the build-side RDD fed Iterator.empty into the native plan and stashed the data in a prebuilt CPU hash table (VeloxBroadcastBuildSideCache -> OpaqueHashTable on the HashJoinNode). CudfHashJoin has no knowledge of OpaqueHashTable and builds from the build-side value stream, so every GPU broadcast join built from an empty stream and silently returned zero rows. When spark.gluten.sql.columnar.cudf=true, stream the deserialized broadcast batches instead (the same path shuffle joins use on GPU) and skip the CPU cache build. Hybrid stays correct: with no cached table the HashJoinNode carries no reusable table, so a CPU-fallback join builds from the stream.
|
Thanks for working on this issue. Adding check and conversion from CudfVector to RowVector do avoid runtime failure. However, in my opinion, it's expected that the output from gpu pipeline should always be converted into I also wonder if making this change general (adding guard on every RowVector retrieval) may burry some real issues. |
|
There are test failures in cpp-test-udf-test PTAL. Thanks! |
your right about the design inside a Velox pipeline, but in Gluten, batches also cross the JNI boundary through channels the auto inserter can't see, and their CPU/GPU is only known at runtime we verified GPU vectors reaching C2R (q15) and CPU vectors reaching GPU ops (q18), plus GPU vectors being silently serialized as 0 rows (the empty-broadcast-join root cause). The guards sit only at those boundary consumers, not everywhere. To avoid hiding real bugs, I'll add a warning log when a guard actually fires, and I can post the q15 operator dump if you want to chase the exact insertion gap |
@ReemaAlzaid This would be helpful. Can you please share it?
This is an issue that can be buried by this conversion. Currently the bhj build once feature is not working in gpu pipeline. We should disable this feature for gpu workload, rather than enable it and add CudfVector to RowVector conversion. |
AdjustStageExecutionMode flipped every VeloxResizeBatchesExec in a GPU stage to GPU mode, including the exchange's direct child. GPU mode repurposes the resizer as the gpu-buffer to cudf table converter for shuffle reads, so at the shuffle-write position it received the transformer's CudfVector batches and failed the buffer-batch cast in GpuBufferBatchResizer. Keep the write-side resizer in CPU mode and materialize device batches in VeloxBatchResizer so the hash shuffle writer consumes host vectors.
…sidency mode materializeVeloxRowVector() now warns when it actually converts a device-resident CudfVector, since a conversion means the cuDF driver adapter did not insert CudfToVelox before the vector left the GPU pipeline. The new conf spark.gluten.sql.columnar.backend.velox.cudf.strictResidency (default false) makes it fail instead of converting so tests and CI can surface conversion gaps at the exact read site instead of masking them.
|
Thanks @marin-ma. Operator dump attached two q15 runs on the current branch with Result: I can no longer reproduce the escape. Every driver whose output leaves the task now ends with a trailing CudfToVelox (the only GPU-terminated pipeline is the join-build sink, which is correct producesGpuOutput=0), and q15 passes value correct in both configs. The original crash On bhj build-once: agreed that's what this PR does. |
Covers the two failure modes fixed in this PR: broadcast hash joins returning empty results on the GPU pipeline, and device CudfVector host reads crashing columnar-to-row. Gated behind GLUTEN_TEST_CUDF=1 since it needs GPU hardware and a cuDF-enabled build.
|
Run Gluten Clickhouse CI on x86 |
68e1457 to
c14e2bd
Compare
d8a9885 to
c14e2bd
Compare
| // use on GPU). Skipping the CPU cache build also keeps hybrid mode correct: | ||
| // VeloxBroadcastBuildSideCache.get finds no table, so the HashJoinNode carries no | ||
| // reusable table and a CPU-fallback join builds from this stream as usual. | ||
| val output = if (isBNL || !offload || GlutenConfig.get.enableColumnarCudf) { |
There was a problem hiding this comment.
Can you extract this change along with the unit test to another PR?
|
@ReemaAlzaid Apologise for missing your previous comment. Here's my understanding and some suggestions: The first issue The second issue |
@marin-ma no worries, thanks for getting back! I Kept the You were right about it hiding things too. Once I removed it, TPC-H on GPU started failing loudly and pointed at two real bugs in the value streams: With that, TPC-H sf1 with allowCpuFallback=false is at 19/22 — the remaining three (q1/q17/q22) Test gating is now the CudfTest tag per your earlier comment, and I deleted the q15-style test since the code it covered is gone. |
shouldn't happen if |
I looked into it and as u said here a GPU pipeline always ends with In q16, the The fix (#12838): broadcast bytes are host resident by nature the only question is where the single host to device upload happens. The deserializer can't know whether the consuming stage is GPU or CPU, so it should not be the one deciding; it now always hands over host batches (via a COLUMNAR_CUDF_ENABLED=false override scoped to its own Runtime the session config is untouched). The residency decision then happens at the consumer, the only place that knows the stage's contract: a GPU stage uploads through |
What changes are proposed in this pull request?
Broadcast hash joins on the cuDF (GPU) backend were silently returning empty
results, and GPU batches were crashing whenever CPU code tried to read them.
This PR fixes both:
1. Broadcast joins built from an empty stream (
VeloxBroadcastBuildSideRDD.scala)For broadcast joins, Gluten puts the build-side data into a prebuilt CPU hash
table and feeds the plan an empty iterator. The CPU join uses that prebuilt
table but the GPU join (
CudfHashJoin) doesn't know it exists, so it builtits hash table from the empty iterator and every broadcast join returned 0 rows.
Fix: when cuDF is enabled, stream the broadcast batches into the plan, same as
shuffle joins already do.
2. GPU vectors crashing on host reads (new
CudfVectorUtils.h+ call sites)A
CudfVectoron the GPU and has no host side children, so host codereading or serializing it saw garbage (the
childAtcrashes). Added a smallhelper that copies the vector to host first, and applied it at every
host read site including the ColumnarToRow converter, which was causing a
JVM segfault in q15.
Needs a Velox build with the multi-column
hash_with_seedfixfacebookincubator/velox#18047
How was this patch tested?
gluten-it queries-comparevs vanilla Spark (2× NVIDIA L40S, Spark 3.5,pure GPU:
allowCpuFallback=false, broadcast joins enabled):(e.g. q21: 82s → 11s)
Remaining failures are pre-existing cuDF expression gaps
(
row_constructor_with_null,substring), not related to this change.cc: @marin-ma @zhouyuan