Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr_build_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ jobs:
org.apache.spark.sql.comet.execution.shuffle.CometNativePositionalRoundRobinSuite
org.apache.spark.sql.comet.execution.shuffle.CometDiskBlockWriterSuite
org.apache.comet.exec.CometShuffleEncryptionSuite
org.apache.comet.exec.CometShuffleChecksumDisabledSuite
org.apache.comet.exec.CometShuffleManagerSuite
org.apache.comet.exec.DisableAQECometShuffleSuite
org.apache.spark.shuffle.comet.CometUnboundedShuffleMemoryAllocatorSuite
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr_build_macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ jobs:
org.apache.spark.sql.comet.execution.shuffle.CometNativePositionalRoundRobinSuite
org.apache.spark.sql.comet.execution.shuffle.CometDiskBlockWriterSuite
org.apache.comet.exec.CometShuffleEncryptionSuite
org.apache.comet.exec.CometShuffleChecksumDisabledSuite
org.apache.comet.exec.CometShuffleManagerSuite
org.apache.comet.exec.DisableAQECometShuffleSuite
org.apache.spark.shuffle.comet.CometUnboundedShuffleMemoryAllocatorSuite
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ public void writeSortedFileNative(boolean isLastFile, boolean tracingEnabled) th
spillInfo.partitionLengths[currentPartition] = written;

// Store the checksum for the current partition.
partitionChecksums[currentPartition] = getChecksum();
if (partitionChecksums.length > 0) {

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.

Nit: the partition-switch block here and the final-partition block below are near copies, and the final one already had this guard, so they drifted apart. Would a small private helper that sets the checksum, calls doSpilling, records the length and stores the checksum keep the guard in one place? Happy to leave that for a follow-up if you'd rather keep this fix small for backporting.

partitionChecksums[currentPartition] = getChecksum();
}
}
currentPartition = partition;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,11 @@ protected long doSpilling(
tracingEnabled);

long written = results[0];
checksum = results[1];
// Without a checksum the native code returns Long.MIN_VALUE here. Keep -1 in that case, or the
// next write by this writer would start computing a checksum that nothing reads.
if (checksumEnabled) {
checksum = results[1];
}
// results[2] = encode + compression time in nanoseconds (measured by native code)
if (encodeNanosAcc != null) {
encodeNanosAcc.addAndGet(results[2]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,34 @@ class CometShuffleEncryptionSuite extends CometTestBase {
}
}

class CometShuffleChecksumDisabledSuite extends CometTestBase {

override protected def sparkConf: SparkConf = {
val conf = super.sparkConf
conf.set("spark.shuffle.checksum.enabled", "false")
}

test("comet columnar shuffle with shuffle checksums disabled") {
// 10 partitions use the bypass merge sort writer, while 300 partitions exceed
// spark.shuffle.sort.bypassMergeThreshold and use the sort-based writer. The lower spill
// threshold makes every map task spill, so the sort-based writer also merges spill files.
Seq(10, 300).foreach { numPartitions =>
Seq(Int.MaxValue, 2000).foreach { spillThreshold =>
withSQLConf(
CometConf.COMET_SHUFFLE_MODE.key -> "jvm",
CometConf.COMET_SHUFFLE_JVM_SPILL_THRESHOLD.key -> spillThreshold.toString) {
val df = spark
.range(0, 100000, 1, 4)
.selectExpr("id", "cast(id as string) as s")
.repartition(numPartitions, col("id"))
checkCometExchange(df, 1, false)

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.

The comment says 300 partitions use the sort-based writer. Would it be worth asserting that, so the test can't silently stop covering it if a threshold changes? checkCometExchange returns the exchanges, so something like assert(exchanges.head.shuffleDependency.shuffleHandle.getClass.getName.contains("CometSerializedShuffleHandle") == (numPartitions > 200)) should work (I haven't run it). The similar checks in Columnar shuffle for large shuffle partition number discard the boolean, so as written they don't assert anything.

checkSparkAnswer(df)
}
}
}
}
}

class CometShuffleManagerSuite extends CometTestBase {

test("should not bypass merge sort if executor cores are too high") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.apache.spark.shuffle.sort

import java.io.File
import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger

import org.scalatest.BeforeAndAfterEach
Expand All @@ -28,7 +30,10 @@ import org.apache.spark.{SparkConf, TaskContext}
import org.apache.spark.executor.ShuffleWriteMetrics
import org.apache.spark.memory.{TaskMemoryManager, TestMemoryManager}
import org.apache.spark.shuffle.comet.{CometShuffleMemoryAllocator, CometShuffleMemoryAllocatorTrait}
import org.apache.spark.sql.catalyst.expressions.UnsafeRow
import org.apache.spark.sql.comet.execution.shuffle.SpillInfo
import org.apache.spark.sql.types._
import org.apache.spark.storage.TempShuffleBlockId
import org.apache.spark.unsafe.Platform
import org.apache.spark.unsafe.UnsafeAlignedOffset

Expand Down Expand Up @@ -79,7 +84,8 @@ class SpillSorterSuite extends AnyFunSuite with BeforeAndAfterEach {
spillCallback: SpillSorter.SpillCallback = () => {},
spills: java.util.LinkedList[org.apache.spark.sql.comet.execution.shuffle.SpillInfo] =
new java.util.LinkedList[org.apache.spark.sql.comet.execution.shuffle.SpillInfo](),
partitionChecksums: Array[Long] = new Array[Long](10)): SpillSorter = {
partitionChecksums: Array[Long] = new Array[Long](10),
checksumAlgorithm: String = "adler32"): SpillSorter = {
val schema = createTestSchema()
val writeMetrics = new ShuffleWriteMetrics()
val taskContext = TaskContext.empty()
Expand All @@ -93,7 +99,7 @@ class SpillSorterSuite extends AnyFunSuite with BeforeAndAfterEach {
"zstd", // compressionCodec
1, // compressionLevel
true, // offsetsAsLengths
"adler32", // checksumAlgorithm
checksumAlgorithm,
partitionChecksums,
writeMetrics,
taskContext,
Expand Down Expand Up @@ -267,6 +273,43 @@ class SpillSorterSuite extends AnyFunSuite with BeforeAndAfterEach {
}
}

test("write sorted file across partitions with shuffle checksums disabled") {
// With spark.shuffle.checksum.enabled=false the sorter gets an empty checksum array and no
// checksum algorithm.
val spills = new java.util.LinkedList[SpillInfo]()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Alias Comet’s SpillInfo to restore Scala 2.12 builds. When compiling the supported Spark 3.4 or 3.5 profile, Spark’s org.apache.spark.shuffle.sort.SpillInfo in this suite’s package hides the newly imported Comet class. This allocation therefore produces the wrong LinkedList type, and passing it to createSpillSorter fails compilation instead of running the regression test. The constructor at line 294 has the same collision. This blocks both profiles’ test compilation and both Celeborn compatibility jobs. Use an import alias such as SpillInfo => CometSpillInfo at both allocations, or fully qualify the Comet type.

Evidence: Exact-head CI reports found: java.util.LinkedList[org.apache.spark.shuffle.sort.SpillInfo], required: java.util.LinkedList[org.apache.spark.sql.comet.execution.shuffle.SpillInfo] at line 276, plus the hidden-import warning. See Spark 3.5 job https://github.com/apache/datafusion-comet/actions/runs/36499297541/job/109186916881 and Spark 3.4 job https://github.com/apache/datafusion-comet/actions/runs/36499297541/job/109186916902. A local JDK 17 compiler probe using Spark 3.5.9 and the checkout’s actual Comet SpillInfo reproduced both mismatches with Scala 2.12.18. Aliasing both uses compiled successfully. Original and aliased forms both compiled with Scala 2.13.17.

val sorter = createSpillSorter(
spills = spills,
partitionChecksums = Array.emptyLongArray,
checksumAlgorithm = null)
val file = File.createTempFile("spill-sorter", ".data")
try {
val numPartitions = 3
val rowSize = UnsafeRow.calculateBitSetWidthInBytes(1) + 8
// Each record is a 4-byte key followed by an UnsafeRow with one int column.
val record = new Array[Byte](4 + rowSize)
val row = new UnsafeRow(1)
row.pointTo(record, Platform.BYTE_ARRAY_OFFSET + 4, rowSize)
sorter.initialCurrentPage(numPartitions * 2 * (record.length + UAO_SIZE))
for (p <- 0 until numPartitions; i <- 0 until 2) {
row.setInt(0, p * 2 + i)
sorter.insertRecord(record, Platform.BYTE_ARRAY_OFFSET, record.length, p)
}

sorter.setSpillInfo(
new SpillInfo(numPartitions, file, TempShuffleBlockId(UUID.randomUUID())))
sorter.writeSortedFileNative(true, false)

assert(spills.size() === 1)
val partitionLengths = spills.getFirst.partitionLengths
assert(partitionLengths.forall(_ > 0))
assert(partitionLengths.sum === file.length())
} finally {
sorter.freeMemory()
sorter.freeArray()
file.delete()
}
}

test("pointer array growth is sized from the array, not from the data pages") {
// Use the unified (off-heap) allocator so that every allocation made by this sorter is
// visible through `getUsed` on an allocator that is private to this test.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import org.apache.spark.executor.{ShuffleWriteMetrics, TaskMetrics}
import org.apache.spark.memory.{SparkOutOfMemoryError, TaskMemoryManager, TestMemoryManager}
import org.apache.spark.shuffle.api.{ShuffleExecutorComponents, ShuffleMapOutputWriter, ShufflePartitionWriter}
import org.apache.spark.shuffle.api.metadata.MapOutputCommitMessage
import org.apache.spark.shuffle.comet.CometShuffleMemoryAllocator
import org.apache.spark.shuffle.comet.{CometShuffleMemoryAllocator, CometShuffleMemoryAllocatorTrait}
import org.apache.spark.shuffle.sort.SpillSorter
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.{UnsafeProjection, UnsafeRow}
Expand All @@ -40,6 +40,8 @@ import org.apache.spark.sql.types.{BinaryType, IntegerType, MetadataBuilder, Str
import org.apache.spark.unsafe.UnsafeAlignedOffset
import org.apache.spark.util.Utils

import org.apache.comet.CometConf

class CometDiskBlockWriterSuite extends AnyFunSuite {

private val schema = StructType(Seq(StructField("a", BinaryType)))
Expand Down Expand Up @@ -216,6 +218,49 @@ class CometDiskBlockWriterSuite extends AnyFunSuite {
}
}

private def newWriter(
file: File,
allocator: CometShuffleMemoryAllocatorTrait,
taskContext: TaskContextImpl,
conf: SparkConf): CometDiskBlockWriter = {
new CometDiskBlockWriter(
file,
allocator,
taskContext,
new UnsafeRowSerializer(1).newInstance(),
schema,
new ShuffleWriteMetrics,
conf,
false,
new JLinkedList[CometDiskBlockWriter]())
}

test("a writer computes no checksum when shuffle checksums are disabled") {
// With spark.shuffle.checksum.enabled=false the bypass merge sort writer never calls
// setChecksum, so every write, including the ones after the first spill, must skip it.
val conf = new SparkConf()
.set("spark.memory.offHeap.enabled", "true")
.set("spark.memory.offHeap.size", "1g")
val tmm = new TaskMemoryManager(new TestMemoryManager(conf), 0L)
val allocator = CometShuffleMemoryAllocator.getInstance(tmm, pageSize)
val tempDir = Utils.createTempDir()
// Spill every two rows so that the writer writes to its file several times.
SQLConf.get.setConfString(CometConf.COMET_SHUFFLE_JVM_SPILL_THRESHOLD.key, "2")
try {
val writer =
newWriter(new File(tempDir, "partition0"), allocator, newTaskContext(tmm, 0L), conf)
val toUnsafe = UnsafeProjection.create(schema)
(0 until 5).foreach(_ => writer.insertRow(toUnsafe(InternalRow(new Array[Byte](16))), 0))
assert(writer.close().length > 0)
assert(writer.getOutputRecords == 5)
assert(writer.getChecksum == -1)

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.

Would it make sense to also run this scenario with checksums enabled? That is the default, and this PR changes the assignment in doSpilling that the enabled path relies on, but I couldn't find a test that checks a checksum value. With writer.setChecksum(Long.MIN_VALUE) and writer.setChecksumAlgo("adler32") before the inserts, I'd expect getChecksum to equal a one-shot java.util.zip.Adler32 over the partition file (I haven't run this). Parameterizing this test over both settings would keep the test count flat.

} finally {
SQLConf.get.unsetConf(CometConf.COMET_SHUFFLE_JVM_SPILL_THRESHOLD.key)
Utils.deleteRecursively(tempDir)
tmm.cleanUpAllAllocatedMemory()
}
}

test("a fatal error during write() frees the task's buffered pages") {
// Spark's ShuffleWriteProcessor only calls stop(false) when write() throws an Exception, so a
// fatal error such as SparkOutOfMemoryError skips it. write() itself must therefore free the
Expand Down
Loading