Skip to content

[VL] Deserialize broadcast build side per the consuming stage's cuDF tag - #12838

Open
ReemaAlzaid wants to merge 4 commits into
apache:mainfrom
ReemaAlzaid:cudf-broadcast-host-deserialize
Open

[VL] Deserialize broadcast build side per the consuming stage's cuDF tag#12838
ReemaAlzaid wants to merge 4 commits into
apache:mainfrom
ReemaAlzaid:cudf-broadcast-host-deserialize

Conversation

@ReemaAlzaid

@ReemaAlzaid ReemaAlzaid commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

What changes are proposed in this pull request?

BuildSideRelation#deserialized built its Runtime from the session conf, so
broadcast batch residency was decided session wide, while the consuming
stage's value stream contract is decided per stage by the cuDF tag. The two
disagree in both directions: an untagged stage receives device-resident
batches on a host contract ValueStreamNode (TPC-H q16), and a tagged stage's
CudfHashJoinBuild requires device input (TPC-DS q95).

Pass the consuming stage's offloadCuda into deserialized instead, so a
cuDF offloaded stage gets the GPU serializer and a non-offloaded stage gets
the host one. This mirrors the shuffle read path, which already selects its
deserializer from the consumer's output type rather than the session conf.

BroadcastUtils keeps the no arg deserialized, which is now host-resident —
it feeds VeloxColumnarToRow and wants host batches in every mode.

How was this patch tested?

Tested on an L40S without #12471 applied: TPC-DS q95 and TPC-H q16 both pass
in hybrid and pure-GPU.

Was this patch authored or co-authored using generative AI tooling?

…uDF conf

BuildSideRelation#deserialized creates its Runtime from the session conf, so
with spark.gluten.sql.columnar.cudf=true VeloxRuntime::createColumnarBatchSerializer
returns the GPU serializer and every broadcast batch is uploaded to the device,
even when the consuming stage was not offloaded to cuDF and planned a
host-contract value stream. TPC-H q16 reproduces this deterministically: its
not-in subquery is a null-aware anti join, which Spark always executes as a
broadcast join even with spark.sql.autoBroadcastJoinThreshold=-1, so the
untagged consumer receives device-resident CudfVectors and fails.

Pass a per-instance COLUMNAR_CUDF_ENABLED=false override so broadcast batches
are always deserialized to host. Both serializer flavors share the same wire
format (the GPU one only overrides deserialize), and cuDF consumers upload
host batches themselves via CudfVectorStream.
@marin-ma

Copy link
Copy Markdown
Contributor

The join stage of BHJ is supported on GPU. We shouldn't set spark.sql.autoBroadcastJoinThreshold=-1.

@marin-ma marin-ma left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my understanding, the gap lies in the VeloxGpuColumnarBatchSerializer is always called regardless of the hash join stage is executed on cpu or gpu. When cudf is enabled, the query fails when the hash join is executed on CPU, but can pass on GPU.

This change will cause the hash join to fail when it is executed on the GPU.

@marin-ma

Copy link
Copy Markdown
Contributor

Tested tpcds q95 locally with a small dataset. Before this change it can pass, but with this change it fails.

GLUTEN_IT_JVM_ARGS=-Xmx5G sbin/gluten-it.sh queries \
      --local --preset=velox --benchmark-type=ds --error-on-memleak --off-heap-size=10g -s=1 --threads=1 --iterations=1 --decimal-as-double=true --enable-history --enable-ui --data-gen=once --extra-conf=spark.gluten.sql.columnar.cudf=true --extra-conf=spark.gluten.sql.debug=true --extra-conf=spark.gluten.sql.debug.cudf=true --extra-conf=spark.gluten.velox.buildHashTableOncePerExecutor.enabled=false --extra-conf=spark.sql.autoBroadcastJoinThreshold=100000 --queries=q95

@ReemaAlzaid

Copy link
Copy Markdown
Contributor Author

Based on my understanding, the gap lies in the VeloxGpuColumnarBatchSerializer is always called regardless of the hash join stage is executed on cpu or gpu. When cudf is enabled, the query fails when the hash join is executed on CPU, but can pass on GPU.

This change will cause the hash join to fail when it is executed on the GPU.

I agree with you on this. CudfValueStream extends CudfOperator, so the adapter marks it as producing GPU output and inserts no CudfFromVelox after it grep -c from-velox on a q95 run is 0. The GPU serializer was the only thing uploading. This PR removes it and puts nothing in its place, so CudfHashJoinBuild gets a host batch and the cast fails. Exactly what you saw.

The missing piece is #12471, which uploads in CudfVectorStream instead. With both applied q95 passes, same plan and same build tables as before one H2D copy, just moved to the consumer

Also on autoBroadcastJoinThreshold=-1; I only used it to isolate q16 to a single broadcast, and I'll re run at the default.

So there are two options we could do:

I lean toward 2. Happy to do either

@marin-ma

Copy link
Copy Markdown
Contributor

I wouldn't agree with the changes in #12471 Cudf related execution shouldn't be introduced into RowVectorStream. The errors should mostly come from the incorrect shape of the pipeline. We should fix the pipeline construction rather than adding the "RowVector from/to CudfVector" conversions in Gluten.

Please continue with solution 2. Based on the current support status, after the fix the correct pipeline for BHJ should be:

GPU stage:
GPU Deserialiser (host to device) -> CudfValueStream -> Cudf pipeline -> CudfToVelox (device to host) -> shuffle write/broadcast serialization/c2r/...

CPU stage:
CPU Deserialiser -> RowVectorStream -> CPU pipeline -> shuffle write/broadcast serialization/c2r/...

@ReemaAlzaid
ReemaAlzaid requested a review from marin-ma August 26, 2026 19:52
@ReemaAlzaid

Copy link
Copy Markdown
Contributor Author

@marin-ma Pushed, the deserializer now follows the consuming stage's offloadCuda

Traced on q95: CudfValueStream → CudfHashJoinBuild on the build side, CudfValueStream → … → CudfToVelox on the probe, 0 CudfFromVelox inserted. Untagged stages take the host deserializer and RowVectorStream

One flag: dropping #12471 exposes q3/q10/q18, failing at CudfTopN::doAddInput TakeOrderedAndProject's runtime built exchange skips AdjustStageExecutionMode. A control run with the old serializer fails identically, so not a regression here, but it's the same wrong pipeline shape. Should I open a separate issue for it? or continue on it from the #12471 patch?

@ReemaAlzaid ReemaAlzaid changed the title [VL] Deserialize broadcast build side on host regardless of session cuDF conf [VL] Deserialize broadcast build side per the consuming stage's cuDF tag Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants