Skip to content

Commit 4fca04e

Browse files
authored
Merge branch 'main' into tagging-2026_07_10
2 parents e927078 + 5ecef3c commit 4fca04e

78 files changed

Lines changed: 12345 additions & 939 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ hs_err_pid*
1313

1414
# IDEA config
1515
.idea/
16+
# Eclipse config
17+
.classpath
18+
.project
19+
.settings/
1620
# vscode config
1721
.vscode
1822
# vscode scala

backends-velox/src-celeborn/main/scala/org/apache/spark/shuffle/VeloxCelebornColumnarBatchSerializer.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ private class CelebornColumnarBatchSerializerInstance(
7474
private val runtime =
7575
Runtimes.contextInstance(BackendsApiManager.getBackendName, "CelebornShuffleReader")
7676

77+
private val jniWrapper = ShuffleReaderJniWrapper.create(runtime)
78+
7779
private val shuffleReaderHandle = {
7880
val allocator: BufferAllocator = ArrowBufferAllocators
7981
.contextInstance(classOf[CelebornColumnarBatchSerializerInstance].getSimpleName)
@@ -91,20 +93,19 @@ private class CelebornColumnarBatchSerializerInstance(
9193
}
9294
val compressionCodecBackend =
9395
GlutenConfig.get.columnarShuffleCodecBackend.orNull
94-
val jniWrapper = ShuffleReaderJniWrapper.create(runtime)
9596
val batchSize = GlutenConfig.get.maxBatchSize
9697
val readerBufferSize = GlutenConfig.get.columnarShuffleReaderBufferSize
9798
val deserializerBufferSize = GlutenConfig.get.columnarSortShuffleDeserializerBufferSize
9899
val enableHashShuffleReaderStreamMerge = VeloxConfig.get.enableHashShuffleReaderStreamMerge
99100
val handle = jniWrapper
100101
.make(
102+
shuffleWriterType.name,
101103
cSchema.memoryAddress(),
102104
compressionCodec,
103105
compressionCodecBackend,
104106
batchSize,
105107
readerBufferSize,
106108
deserializerBufferSize,
107-
shuffleWriterType.name,
108109
enableHashShuffleReaderStreamMerge
109110
)
110111
// Close shuffle reader instance as lately as the end of task processing,
@@ -246,14 +247,14 @@ private class CelebornColumnarBatchSerializerInstance(
246247
}
247248

248249
private def close0(): Unit = {
250+
jniWrapper.stop(shuffleReaderHandle)
249251
if (numBatchesTotal > 0) {
250252
readBatchNumRows.set(numRowsTotal.toDouble / numBatchesTotal)
251253
}
252254
numOutputRows += numRowsTotal
253255
if (wrappedOut != null) {
254256
wrappedOut.close()
255257
}
256-
streamReader.close()
257258
if (cb != null) {
258259
cb.close()
259260
}

backends-velox/src-delta33/main/scala/org/apache/spark/sql/delta/perf/GlutenDeltaOptimizedWriterExec.scala

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import org.apache.gluten.backendsapi.velox.VeloxBatchType
2121
import org.apache.gluten.config.GlutenConfig
2222
import org.apache.gluten.execution.{ValidatablePlan, ValidationResult}
2323
import org.apache.gluten.extension.columnar.transition.Convention
24+
import org.apache.gluten.sql.shims.SparkShimLoader
2425
import org.apache.gluten.vectorized.ColumnarBatchSerializerInstance
2526

2627
// scalastyle:off import.ordering.noEmptyLine
@@ -316,38 +317,63 @@ private class GlutenOptimizedWriterShuffleReader(
316317
case _ =>
317318
SparkEnv.get.serializerManager
318319
}
319-
val wrappedStreams = new ShuffleBlockFetcherIterator(
320-
context,
321-
SparkEnv.get.blockManager.blockStoreClient,
322-
SparkEnv.get.blockManager,
323-
SparkEnv.get.mapOutputTracker,
324-
blocks,
325-
serializerManager.wrapStream,
326-
// Note: we use getSizeAsMb when no suffix is provided for backwards compatibility
327-
SparkEnv.get.conf.get(config.REDUCER_MAX_SIZE_IN_FLIGHT) * 1024 * 1024,
328-
SparkEnv.get.conf.get(config.REDUCER_MAX_REQS_IN_FLIGHT),
329-
SparkEnv.get.conf.get(config.REDUCER_MAX_BLOCKS_IN_FLIGHT_PER_ADDRESS),
330-
SparkEnv.get.conf.get(config.MAX_REMOTE_BLOCK_SIZE_FETCH_TO_MEM),
331-
SparkEnv.get.conf.get(config.SHUFFLE_MAX_ATTEMPTS_ON_NETTY_OOM),
332-
SparkEnv.get.conf.get(config.SHUFFLE_DETECT_CORRUPT),
333-
SparkEnv.get.conf.get(config.SHUFFLE_DETECT_CORRUPT_MEMORY),
334-
SparkEnv.get.conf.get(config.SHUFFLE_CHECKSUM_ENABLED),
335-
SparkEnv.get.conf.get(config.SHUFFLE_CHECKSUM_ALGORITHM),
336-
readMetrics,
337-
false
338-
).toCompletionIterator
339320

340321
// Create a key/value iterator for each stream
341322
val recordIter = dep match {
342323
case columnarDep: ColumnarShuffleDependency[Int, ColumnarBatch, ColumnarBatch] =>
343-
// If the dependency is a ColumnarShuffleDependency, we use the columnar serializer.
324+
val shuffleBlockFetcherIterator =
325+
SparkShimLoader.getSparkShims.getShuffleBlockFetcherIterator(
326+
ShuffleBlockFetcherIteratorParams(
327+
context,
328+
SparkEnv.get.blockManager.blockStoreClient,
329+
SparkEnv.get.blockManager,
330+
SparkEnv.get.mapOutputTracker,
331+
blocks,
332+
serializerManager.wrapStream,
333+
// Note: we use getSizeAsMb when no suffix is provided for backwards compatibility
334+
SparkEnv.get.conf.get(config.REDUCER_MAX_SIZE_IN_FLIGHT) * 1024 * 1024,
335+
SparkEnv.get.conf.get(config.REDUCER_MAX_REQS_IN_FLIGHT),
336+
SparkEnv.get.conf.get(config.REDUCER_MAX_BLOCKS_IN_FLIGHT_PER_ADDRESS),
337+
SparkEnv.get.conf.get(config.MAX_REMOTE_BLOCK_SIZE_FETCH_TO_MEM),
338+
SparkEnv.get.conf.get(config.SHUFFLE_MAX_ATTEMPTS_ON_NETTY_OOM),
339+
SparkEnv.get.conf.get(config.SHUFFLE_DETECT_CORRUPT),
340+
SparkEnv.get.conf.get(config.SHUFFLE_DETECT_CORRUPT_MEMORY),
341+
SparkEnv.get.conf.get(config.SHUFFLE_CHECKSUM_ENABLED),
342+
SparkEnv.get.conf.get(config.SHUFFLE_CHECKSUM_ALGORITHM),
343+
readMetrics,
344+
doBatchFetch = false
345+
))
344346
columnarDep.serializer
345347
.newInstance()
346348
.asInstanceOf[ColumnarBatchSerializerInstance]
347-
.deserializeStreams(wrappedStreams)
349+
.deserializeStreams(
350+
shuffleBlockFetcherIterator,
351+
shuffleBlockFetcherIterator.onComplete)
348352
.asKeyValueIterator
349353
case _ =>
354+
val wrappedStreams = new ShuffleBlockFetcherIterator(
355+
context,
356+
SparkEnv.get.blockManager.blockStoreClient,
357+
SparkEnv.get.blockManager,
358+
SparkEnv.get.mapOutputTracker,
359+
blocks,
360+
serializerManager.wrapStream,
361+
// Note: we use getSizeAsMb when no suffix is provided for backwards compatibility
362+
SparkEnv.get.conf.get(config.REDUCER_MAX_SIZE_IN_FLIGHT) * 1024 * 1024,
363+
SparkEnv.get.conf.get(config.REDUCER_MAX_REQS_IN_FLIGHT),
364+
SparkEnv.get.conf.get(config.REDUCER_MAX_BLOCKS_IN_FLIGHT_PER_ADDRESS),
365+
SparkEnv.get.conf.get(config.MAX_REMOTE_BLOCK_SIZE_FETCH_TO_MEM),
366+
SparkEnv.get.conf.get(config.SHUFFLE_MAX_ATTEMPTS_ON_NETTY_OOM),
367+
SparkEnv.get.conf.get(config.SHUFFLE_DETECT_CORRUPT),
368+
SparkEnv.get.conf.get(config.SHUFFLE_DETECT_CORRUPT_MEMORY),
369+
SparkEnv.get.conf.get(config.SHUFFLE_CHECKSUM_ENABLED),
370+
SparkEnv.get.conf.get(config.SHUFFLE_CHECKSUM_ALGORITHM),
371+
readMetrics,
372+
false
373+
).toCompletionIterator
374+
350375
val serializerInstance = dep.serializer.newInstance()
376+
351377
// Create a key/value iterator for each stream
352378
wrappedStreams.flatMap {
353379
case (blockId, wrappedStream) =>
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.spark.sql.execution.benchmark
18+
19+
import org.apache.gluten.delta.DeltaDeletionVectorScanInfo
20+
import org.apache.gluten.extension.DeltaPostTransformRules
21+
22+
import org.apache.spark.benchmark.Benchmark
23+
import org.apache.spark.sql.SparkSession
24+
import org.apache.spark.sql.delta.DeltaLog
25+
26+
import org.apache.hadoop.fs.Path
27+
28+
/**
29+
* Benchmark for Delta Lake planning-time operations in Gluten.
30+
*
31+
* Measures two hot paths that our performance optimizations target:
32+
*
33+
* 1. '''DV Materialization''' (`DeltaDeletionVectorScanInfo.normalize`): resolves table paths,
34+
* loads DV bitmaps from storage, and serializes them into split metadata. Our optimizations
35+
* (caching table path, Hadoop conf, DV store across files) target this path.
36+
* 2. '''Post-transform rule application''' (`DeltaPostTransformRules.rules`): traverses the
37+
* physical plan to strip DV synthetic columns, push down input_file_name, and apply column
38+
* mapping. Our optimizations (early-exit guard, shallow child check, pre-computed names,
39+
* batched attribute mapping) target this path.
40+
*
41+
* To run:
42+
* {{{
43+
* bin/spark-submit --class org.apache.spark.sql.execution.benchmark.DeltaPlanningBenchmark \
44+
* --jars <spark-core-test-jar>,<gluten-backends-velox-jar>
45+
* }}}
46+
*
47+
* Or via Maven (from the backends-velox module):
48+
* {{{
49+
* ./build/mvn test -pl backends-velox -Pspark-3.5 -Pbackends-velox -Pdelta -Pjava-17 \
50+
* -Dtest=none -DfailIfNoTests=false \
51+
* -Dsuites="org.apache.spark.sql.execution.benchmark.DeltaPlanningBenchmark"
52+
* }}}
53+
*/
54+
object DeltaPlanningBenchmark extends SqlBasedBenchmark {
55+
56+
override def getSparkSession: SparkSession = {
57+
SparkSession
58+
.builder()
59+
.master("local[1]")
60+
.appName("DeltaPlanningBenchmark")
61+
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
62+
.config(
63+
"spark.sql.catalog.spark_catalog",
64+
"org.apache.spark.sql.delta.catalog.DeltaCatalog")
65+
.config("spark.plugins", "org.apache.gluten.GlutenPlugin")
66+
.config("spark.memory.offHeap.enabled", "true")
67+
.config("spark.memory.offHeap.size", "1024MB")
68+
.config("spark.ui.enabled", "false")
69+
.config("spark.default.parallelism", "1")
70+
.getOrCreate()
71+
}
72+
73+
private val numFiles =
74+
spark.sparkContext.conf.getInt("spark.gluten.benchmark.delta.numFiles", 100)
75+
private val rowsPerFile =
76+
spark.sparkContext.conf.getInt("spark.gluten.benchmark.delta.rowsPerFile", 10000)
77+
private val benchmarkIters =
78+
spark.sparkContext.conf.getInt("spark.gluten.benchmark.iterations", 5)
79+
80+
override def runBenchmarkSuite(mainArgs: Array[String]): Unit = {
81+
runDvMaterializationBenchmark()
82+
runPostTransformRulesBenchmark()
83+
runNonDeltaRulesOverheadBenchmark()
84+
}
85+
86+
/**
87+
* Benchmarks DeltaDeletionVectorScanInfo.normalize() -- the critical path that loads DVs from
88+
* storage on the driver. Measures how caching table path + DV store reduces overhead.
89+
*/
90+
private def runDvMaterializationBenchmark(): Unit = {
91+
val benchmark = new Benchmark(
92+
s"DV Materialization (normalize) - $numFiles files",
93+
numFiles.toLong,
94+
minNumIters = benchmarkIters,
95+
output = output)
96+
97+
withDeltaTableWithDVs(numFiles, rowsPerFile) {
98+
(path, partitionedFiles) =>
99+
benchmark.addCase(s"normalize() - $numFiles DV files", benchmarkIters) {
100+
_ =>
101+
DeltaDeletionVectorScanInfo.normalize(
102+
partitionColumnCount = 0,
103+
partitionFiles = partitionedFiles)
104+
}
105+
106+
benchmark.run()
107+
}
108+
}
109+
110+
/**
111+
* Benchmarks DeltaPostTransformRules application on a Delta plan with DV columns. Measures the
112+
* combined cost of DV stripping, input_file pushdown, and column mapping.
113+
*/
114+
private def runPostTransformRulesBenchmark(): Unit = {
115+
val benchmark = new Benchmark(
116+
"Post-transform rules (Delta plan)",
117+
1L,
118+
minNumIters = benchmarkIters,
119+
output = output)
120+
121+
withDeltaTableWithDVs(numFiles = 10, rowsPerFile = 1000) {
122+
(path, _) =>
123+
val df = spark.read.format("delta").load(path)
124+
// Force planning to get the executed plan with DeltaScanTransformer
125+
val plan = df.queryExecution.executedPlan
126+
127+
benchmark.addCase("apply rules (Delta plan with DV)", benchmarkIters) {
128+
_ =>
129+
val result = DeltaPostTransformRules.rules.foldLeft(plan) {
130+
(p, rule) => rule(p)
131+
}
132+
// Prevent dead code elimination
133+
assert(result != null)
134+
}
135+
136+
benchmark.run()
137+
}
138+
}
139+
140+
/**
141+
* Benchmarks post-transform rules on a non-Delta plan to verify zero overhead from the early-exit
142+
* guard. This is the "control" case showing that non-Delta queries don't pay for Delta rule
143+
* traversals.
144+
*/
145+
private def runNonDeltaRulesOverheadBenchmark(): Unit = {
146+
val benchmark = new Benchmark(
147+
"Post-transform rules (non-Delta plan)",
148+
1L,
149+
minNumIters = benchmarkIters,
150+
output = output)
151+
152+
withTempPath {
153+
p =>
154+
// Create a parquet table (not Delta)
155+
val path = p.getCanonicalPath
156+
spark
157+
.range(0, 100000, 1, numPartitions = 10)
158+
.selectExpr("id", "id * 2 as value", "cast(id as string) as name")
159+
.write
160+
.parquet(path)
161+
162+
val df = spark.read.parquet(path)
163+
val plan = df.queryExecution.executedPlan
164+
165+
benchmark.addCase("apply rules (non-Delta parquet plan)", benchmarkIters) {
166+
_ =>
167+
val result = DeltaPostTransformRules.rules.foldLeft(plan) {
168+
(p, rule) => rule(p)
169+
}
170+
assert(result != null)
171+
}
172+
173+
benchmark.run()
174+
}
175+
}
176+
177+
/**
178+
* Creates a Delta table with deletion vectors and provides the partitioned files for direct DV
179+
* materialization benchmarking.
180+
*/
181+
private def withDeltaTableWithDVs(numFiles: Int, rowsPerFile: Int)(
182+
f: (String, Seq[org.apache.spark.sql.execution.datasources.PartitionedFile]) => Unit
183+
): Unit = {
184+
withTempPath {
185+
p =>
186+
val path = p.getCanonicalPath
187+
val totalRows = numFiles.toLong * rowsPerFile
188+
189+
// Write data across multiple files
190+
spark
191+
.range(0, totalRows, 1, numPartitions = numFiles)
192+
.selectExpr("id", "id * 2 as value")
193+
.write
194+
.format("delta")
195+
.save(path)
196+
197+
// Enable DVs and delete some rows to create DV entries
198+
spark.sql(s"""ALTER TABLE delta.`$path`
199+
SET TBLPROPERTIES ('delta.enableDeletionVectors' = true)""")
200+
// Delete ~10% of rows to generate DVs on most files
201+
spark.sql(s"DELETE FROM delta.`$path` WHERE id % 10 = 0")
202+
203+
// Extract partitioned files with DV metadata
204+
val deltaLog = DeltaLog.forTable(spark, new Path(path))
205+
val snapshot = deltaLog.update()
206+
val allFiles = snapshot.allFiles.collect()
207+
208+
import org.apache.spark.paths.SparkPath
209+
import org.apache.spark.sql.catalyst.InternalRow
210+
import org.apache.spark.sql.delta.GlutenDeltaParquetFileFormat
211+
import org.apache.spark.sql.execution.datasources.PartitionedFile
212+
213+
val partitionedFiles = allFiles.map {
214+
dataFile =>
215+
val metadata: Map[String, Object] =
216+
if (dataFile.deletionVector != null) {
217+
Map(
218+
GlutenDeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_ID_ENCODED ->
219+
dataFile.deletionVector.serializeToBase64(),
220+
GlutenDeltaParquetFileFormat.FILE_ROW_INDEX_FILTER_TYPE -> "IF_CONTAINED"
221+
)
222+
} else {
223+
Map.empty[String, Object]
224+
}
225+
PartitionedFile(
226+
partitionValues = InternalRow.empty,
227+
filePath = SparkPath.fromPath(new Path(path, dataFile.path)),
228+
start = 0L,
229+
length = dataFile.size,
230+
fileSize = dataFile.size,
231+
otherConstantMetadataColumnValues = metadata
232+
)
233+
}.toSeq
234+
235+
f(path, partitionedFiles)
236+
}
237+
}
238+
}

0 commit comments

Comments
 (0)