sunchao commented on code in PR #57528:
URL: https://github.com/apache/spark/pull/57528#discussion_r3714053339


##########
core/src/main/scala/org/apache/spark/scheduler/MapStatus.scala:
##########
@@ -286,40 +334,63 @@ private[spark] object HighlyCompressedMapStatus {
       Option(SparkEnv.get)
         .map(_.conf.get(config.SHUFFLE_ACCURATE_BLOCK_THRESHOLD))
         .getOrElse(config.SHUFFLE_ACCURATE_BLOCK_THRESHOLD.defaultValue.get)
+    // Sizes at or above `threshold` are recorded accurately. At most
+    // SHUFFLE_MAX_ACCURATE_SKEWED_BLOCK_NUMBER of them are strictly larger 
than `skewCutoff`, and
+    // those are held as ids and sizes. There can be arbitrarily many blocks 
of exactly
+    // `skewCutoff`, but they all share that one size, so only their 
membership is recorded.
+    var skewCutoff = Long.MaxValue
+    var recordSkewedTies = false
     val threshold =
       if (accurateBlockSkewedFactor > 0) {
-        val sortedSizes = uncompressedSizes.sorted
-        val medianSize: Long = Utils.median(sortedSizes, true)
         val maxAccurateSkewedBlockNumber =
           Math.min(
             Option(SparkEnv.get)
               .map(_.conf.get(config.SHUFFLE_MAX_ACCURATE_SKEWED_BLOCK_NUMBER))
               
.getOrElse(config.SHUFFLE_MAX_ACCURATE_SKEWED_BLOCK_NUMBER.defaultValue.get),
             totalNumBlocks
           )
+        // Only two order statistics are needed here, so they are selected in 
O(totalNumBlocks)
+        // instead of sorting the sizes, which every map task would otherwise 
pay for.
+        val sizes = uncompressedSizes.clone()
+        val medianSize: Long = Utils.medianInPlace(sizes)
+        val firstAccurateIdx = totalNumBlocks - maxAccurateSkewedBlockNumber
+        skewCutoff = Utils.nthSmallest(sizes, firstAccurateIdx)
         val skewSizeThreshold =
-          Math.max(
-            medianSize * accurateBlockSkewedFactor,
-            sortedSizes(totalNumBlocks - maxAccurateSkewedBlockNumber).toDouble
-          )
-        Math.min(shuffleAccurateBlockThreshold.toDouble, skewSizeThreshold)
+          Math.max(medianSize * accurateBlockSkewedFactor, skewCutoff.toDouble)
+        val skewThreshold = Math.min(shuffleAccurateBlockThreshold.toDouble, 
skewSizeThreshold)
+        // Every map task of a shuffle sees the same block size distribution, 
so any rule that
+        // records only some of the blocks tied at the cutoff hides the same 
reducers in every map
+        // status, and MapOutputTracker sums those. The hidden ones fall back 
to `avgSize`, which
+        // they raise in the process, so AQE's skew threshold moves up along 
with the sizes it is
+        // compared against. Recording all of the ties is what keeps them 
visible.
+        recordSkewedTies = skewCutoff > 0 && skewThreshold <= 
skewCutoff.toDouble
+        skewThreshold
       } else {
         // Disable skew detection if accurateBlockSkewedFactor <= 0
         shuffleAccurateBlockThreshold.toDouble
       }
 
-    val hugeBlockSizes = mutable.Map.empty[Int, Byte]
+    val hugeBlockIds = mutable.ArrayBuilder.make[Int]
+    val hugeBlockSizes = mutable.ArrayBuilder.make[Byte]
+    val skewedBlocks = new RoaringBitmap()
     while (i < totalNumBlocks) {
       val size = uncompressedSizes(i)
       if (size > 0) {
         numNonEmptyBlocks += 1
         // Huge blocks are not included in the calculation for average size, 
thus size for smaller
-        // blocks is more accurate.
-        if (size < threshold) {
+        // blocks is more accurate. Blocks tied at the cutoff are excluded 
from it as well: they
+        // are recorded accurately, so folding them into the average would 
both lose nothing and
+        // inflate the size reported for the blocks that really are average.
+        val isHuge = size >= shuffleAccurateBlockThreshold ||
+          (size >= threshold && size > skewCutoff)
+        if (isHuge) {
+          hugeBlockIds += i
+          hugeBlockSizes += MapStatus.compressSize(size)
+        } else if (recordSkewedTies && size == skewCutoff) {
+          skewedBlocks.add(i)

Review Comment:
   [P2] Bound cutoff-tie bitmap growth for non-contiguous reducer IDs
   
   Recording every `skewCutoff` tie in `skewedBlocks` means 
`spark.shuffle.maxAccurateSkewedBlockNumber=100` no longer bounds retained or 
serialized metadata. The new regression test makes all 24,999 tied reducer IDs 
consecutive, so `runOptimize()` compresses them into one run; that is not 
representative of arbitrary reducer IDs.
   
   Using this exact head and its declared RoaringBitmap 1.6.10 dependency, with 
50,000 reducers, 25,001 blocks of 1 KiB, and 24,999 blocks of 8 KiB:
   
   - Consecutive tied reducer IDs: bitmap retained payload 18 bytes; serialized 
bitmap 15 bytes.
   - Alternating tied reducer IDs with the identical block-size distribution: 
retained payload 8,202 bytes; serialized bitmap 8,208 bytes, after the same 
`trim()` and `runOptimize()` calls as production.
   
   Thus 10,000 mappers retain at least 82,020,000 bitmap bytes, and 100,000 
mappers retain at least 820,200,000 bytes, despite the configured 100-entry 
cap. At 1,048,576 alternating reducers, the measured bitmap is 131,112 bytes 
per map status, or 1,311,120,000 retained bytes for 10,000 mappers. These are 
bitmap-payload lower bounds; object overhead and cached serialized/broadcast 
copies are additional, and serialized sizes above are before compression.
   
   The current `retained < 8 * 1024` test would already fail for the 
50,000-partition alternating layout. Please bound tie metadata independently of 
reducer count while preserving AQE visibility, and add an 
interleaved/fragmented-cutoff-tie retained-memory and serialization regression.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to