Qing Fu created SPARK-59510:
-------------------------------

             Summary: Derive UnsafeExternalSorter spill merge factor from a 
memory budget
                 Key: SPARK-59510
                 URL: https://issues.apache.org/jira/browse/SPARK-59510
             Project: Spark
          Issue Type: Improvement
          Components: Spark Core
    Affects Versions: 5.0.0
            Reporter: Qing Fu


SPARK-56410 added bounded multi-round merging to {{UnsafeExternalSorter}}, 
controlled by {{spark.unsafe.sorter.spill.merge.factor}}: the maximum number of 
spill files merged in a single round.

The quantity the merge phase actually needs to bound is memory, not a file 
count. Each open {{UnsafeSorterSpillReader}} holds a read-ahead buffer, a 
decompression buffer and a buffer sized for the largest record it has returned, 
so the memory a round needs is roughly {{factor * perReaderBytes}}. Choosing a 
good {{factor}} therefore means knowing {{perReaderBytes}}, which is a function 
of settings the user has already configured.

Proposal: allow the factor to be derived from a memory budget. Spark already 
exposes the inputs:

* {{spark.unsafe.sorter.spill.reader.buffer.size}} (default 1m) - the reader's 
I/O buffer
* {{spark.unsafe.sorter.spill.read.ahead.enabled}} (default true) - read-ahead 
holds a second buffer

so the derivation needs only one new config, the budget itself:

{code}
perReader = readerBufferSize * (readAheadEnabled ? 2 : 1) + recordAllowance
factor    = max(2, maxMemory / perReader)
{code}

An explicitly configured {{spark.unsafe.sorter.spill.merge.factor}} would 
continue to win; the derivation would only apply when the user asks for it.

Two related questions, worth settling here rather than in review:

1. Should bounded merging be enabled by default? 
{{spark.unsafe.sorter.spill.merge.factor}} currently defaults to {{-1}} 
(disabled), while the {{UnsafeSorterBoundedSpillMerger}} class javadoc already 
describes "the default factor of 64". From operating a large Spark deployment 
with multi-round merging enabled by default, we have seen a substantial 
reduction in merge-phase OOMs and in the compute cost of the affected 
workloads, with no correctness or performance regression observed across a 
broad benchmark set. Tasks that spill hundreds of times are exactly the ones 
that most need the sorter, and today such a task can write every spill 
successfully and then OOM in the merge, with no recovery other than a larger 
executor.

2. {{UnsafeSorterIterator.getNumRecords()}} returns {{int}}. Widening it is the 
real fix for the record-count ceiling that 
{{UnsafeSorterBoundedSpillMerger.partitionWriters}} currently works around, but 
it changes the spill file header format, so it likely deserves its own 
discussion.

Happy to put up a PR for the memory-budget derivation if the approach sounds 
reasonable.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to