Skip to content

Commit e1deb81

Browse files
authored
[fix] admit TopNRowNumber output before non-reclaimable phase (#601)
1 parent eab7e1b commit e1deb81

8 files changed

Lines changed: 134 additions & 8 deletions

File tree

bolt/common/memory/sparksql/ExecutionMemoryPool.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,20 @@ uint64_t ExecutionMemoryPool::borrowFromRssWatermarkBytes(
408408
return it == instance()->borrowFromRssWatermarkBytes_.end() ? 0 : it->second;
409409
}
410410

411+
uint64_t ExecutionMemoryPool::getConfiguredMemoryPerTask() {
412+
if (!inited()) {
413+
return 0;
414+
}
415+
416+
auto self = instance();
417+
MemoryMutexGuard guard(self->lock_);
418+
const auto activeTasks = self->memoryForTask_.size();
419+
if (!self->poolExtendSize_.has_value() || activeTasks == 0) {
420+
return 0;
421+
}
422+
return self->poolSize_.value_or(0) / activeTasks;
423+
}
424+
411425
std::ostream& operator<<(std::ostream& os, const ExecutionMemoryPool& pool) {
412426
os << "ExecutionMemoryPool(poolSize=" << pool.poolSize_.value_or(0)
413427
<< ", poolExtendSize=" << pool.poolExtendSize_.value_or(0)

bolt/common/memory/sparksql/ExecutionMemoryPool.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ class ExecutionMemoryPool final
137137

138138
static uint64_t borrowFromRssWatermarkBytes(int64_t taskAttemptId);
139139

140+
static uint64_t getConfiguredMemoryPerTask();
141+
140142
// For testing only, reset pool size to test new cases with new pool size
141143
static void testingResetPoolSize(int64_t newSize);
142144

bolt/exec/HashBuild.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,8 @@ uint64_t HashBuild::probeAdmissionExtraReservationBytes(
12721272
<< succinctBytes(pressure.reclaimWatermarkBytes)
12731273
<< ", borrowFromRssWatermarkBytes="
12741274
<< succinctBytes(pressure.borrowFromRssWatermarkBytes)
1275+
<< ", configuredTaskMemoryQuotaBytes="
1276+
<< succinctBytes(pressure.configuredTaskMemoryQuotaBytes)
12751277
<< ", pressureWatermarkBytes="
12761278
<< succinctBytes(pressureWatermarkBytes) << ", details "
12771279
<< task->memoryPressureDetails();

bolt/exec/MemoryPressureUtils.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,18 @@ MemoryPressureSnapshot snapshot(
3232
uint64_t reclaimWatermarkBytes,
3333
const std::optional<int64_t>& sparkTaskAttemptId) {
3434
uint64_t borrowFromRssWatermarkBytes = 0;
35+
uint64_t configuredTaskMemoryQuotaBytes = 0;
3536
if (sparkTaskAttemptId.has_value()) {
3637
borrowFromRssWatermarkBytes =
3738
memory::sparksql::ExecutionMemoryPool::borrowFromRssWatermarkBytes(
3839
*sparkTaskAttemptId);
40+
configuredTaskMemoryQuotaBytes =
41+
memory::sparksql::ExecutionMemoryPool::getConfiguredMemoryPerTask();
3942
}
4043
return MemoryPressureSnapshot{
41-
reclaimWatermarkBytes, borrowFromRssWatermarkBytes};
44+
reclaimWatermarkBytes,
45+
borrowFromRssWatermarkBytes,
46+
configuredTaskMemoryQuotaBytes};
4247
}
4348

4449
std::optional<ScopedMemoryExpansionGuard> maybeScopedDisableMemoryExpansion(

bolt/exec/MemoryPressureUtils.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ class ScopedMemoryExpansionGuard {
6363
struct MemoryPressureSnapshot {
6464
uint64_t reclaimWatermarkBytes{0};
6565
uint64_t borrowFromRssWatermarkBytes{0};
66+
uint64_t configuredTaskMemoryQuotaBytes{0};
6667

6768
uint64_t admissionWatermarkBytes() const {
6869
if (reclaimWatermarkBytes == 0) {
69-
return borrowFromRssWatermarkBytes;
70+
return borrowFromRssWatermarkBytes == 0 ? configuredTaskMemoryQuotaBytes
71+
: borrowFromRssWatermarkBytes;
7072
}
7173
if (borrowFromRssWatermarkBytes == 0) {
7274
return reclaimWatermarkBytes;

bolt/exec/TopNRowNumber.cpp

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -293,16 +293,21 @@ void TopNRowNumber::processInputRow(vector_size_t index, TopRows& partition) {
293293
}
294294

295295
void TopNRowNumber::noMoreInput() {
296-
Operator::noMoreInput();
297-
298296
updateEstimatedOutputRowSize();
299297

298+
if (spiller_ == nullptr) {
299+
ensureOutputFits();
300+
}
301+
302+
Operator::noMoreInput();
303+
300304
outputBatchSize_ = outputBatchRows(estimatedOutputRowSize_);
301305
if (spiller_ != nullptr) {
302-
// Spill remaining data to avoid running out of memory while sort-merging
303-
// spilled data.
304-
spill();
305-
306+
if (data_->numRows() > 0) {
307+
// Spill remaining data to avoid running out of memory while sort-merging
308+
// spilled data.
309+
spill();
310+
}
306311
BOLT_CHECK_NULL(merge_);
307312
auto spillPartition = spiller_->finishSpill();
308313
merge_ = spillPartition.createOrderedReader(pool());
@@ -312,6 +317,59 @@ void TopNRowNumber::noMoreInput() {
312317
}
313318
}
314319

320+
void TopNRowNumber::ensureOutputFits() {
321+
if (abandonedPartial_ || !spillEnabled() || table_ == nullptr ||
322+
data_->numRows() == 0) {
323+
return;
324+
}
325+
326+
const auto currentBytes = pool()->currentBytes();
327+
const auto* task = operatorCtx_->task().get();
328+
const auto pressure = task->memoryPressureSnapshot();
329+
const auto pressureWatermarkBytes = pressure.admissionWatermarkBytes();
330+
const auto& queryConfig = operatorCtx_->driverCtx()->queryConfig();
331+
const uint64_t outputAdmissionLimit =
332+
queryConfig.preferredOutputBatchBytes() *
333+
queryConfig.outputBatchMemoryReservationMultiple();
334+
const auto admissionReservationBytes =
335+
(pressureWatermarkBytes != 0 &&
336+
currentBytes >
337+
pressureWatermarkBytes * queryConfig.memoryPressureWatermarkRatio())
338+
? pressureWatermarkBytes
339+
: std::min<uint64_t>(currentBytes / 2, outputAdmissionLimit);
340+
341+
if (admissionReservationBytes == 0) {
342+
return;
343+
}
344+
345+
{
346+
Operator::ReclaimableSectionGuard guard(this);
347+
const auto scopedDisableMemoryExpansion =
348+
task->maybeScopedDisableMemoryExpansion();
349+
LOG(INFO) << name() << " ensureOutputFits: try to reserve "
350+
<< succinctBytes(admissionReservationBytes)
351+
<< " for output admission, " << pool()->name() << "["
352+
<< succinctBytes(pool()->currentBytes()) << ", "
353+
<< succinctBytes(pool()->reservedBytes()) << ", "
354+
<< succinctBytes(pool()->capacity()) << "]"
355+
<< ", pressureWatermarkBytes="
356+
<< succinctBytes(pressureWatermarkBytes)
357+
<< ", outputAdmissionLimit="
358+
<< succinctBytes(outputAdmissionLimit)
359+
<< ", memoryPressure=" << task->memoryPressureDetails();
360+
if (pool()->maybeReserve(admissionReservationBytes)) {
361+
pool()->release();
362+
return;
363+
}
364+
}
365+
366+
LOG(WARNING) << name() << " Failed to reserve "
367+
<< succinctBytes(admissionReservationBytes)
368+
<< " before producing output for memory pool " << pool()->name()
369+
<< ", usage: " << succinctBytes(pool()->currentBytes())
370+
<< ", reservation: " << succinctBytes(pool()->reservedBytes());
371+
}
372+
315373
void TopNRowNumber::updateEstimatedOutputRowSize() {
316374
const auto optionalRowSize = data_->estimateRowSize();
317375
if (!optionalRowSize.has_value()) {

bolt/exec/TopNRowNumber.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ class TopNRowNumber : public Operator {
183183
// Called in noMoreInput() and spill().
184184
void updateEstimatedOutputRowSize();
185185

186+
// Tries to reserve output admission memory before transitioning to output. If
187+
// memory is under pressure, this can trigger arbitration while this operator
188+
// is still reclaimable.
189+
void ensureOutputFits();
190+
186191
// Sorts, spills and clears all of 'data_'. Clears 'table_'.
187192
void spill();
188193

bolt/exec/tests/TopNRowNumberTest.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,44 @@ TEST_F(TopNRowNumberTest, preservesOrderAcrossOutputBatches) {
210210
.assertResults(sql, std::vector<uint32_t>{0, 1});
211211
}
212212

213+
TEST_F(TopNRowNumberTest, doesNotSpillBeforeOutputWithoutMemoryPressure) {
214+
const vector_size_t size = 1'000;
215+
auto data = split(
216+
makeRowVector({
217+
makeFlatVector<int64_t>(size, [](auto row) { return row % 100; }),
218+
makeFlatVector<int64_t>(size, [](auto row) { return size - row; }),
219+
makeFlatVector<int64_t>(size, [](auto row) { return row; }),
220+
}),
221+
10);
222+
223+
createDuckDbTable(data);
224+
225+
core::PlanNodeId topNRowNumberId;
226+
auto plan = PlanBuilder()
227+
.values(data)
228+
.topNRowNumber({"c0"}, {"c1"}, 10, true)
229+
.capturePlanNodeId(topNRowNumberId)
230+
.planNode();
231+
232+
auto spillDirectory = exec::test::TempDirectoryPath::create();
233+
auto task = AssertQueryBuilder(plan, duckDbQueryRunner_)
234+
.config(core::QueryConfig::kSpillEnabled, "true")
235+
.config(core::QueryConfig::kTopNRowNumberSpillEnabled, "true")
236+
.config(core::QueryConfig::kPreferredOutputBatchBytes, "128")
237+
.spillDirectory(spillDirectory->path)
238+
.assertResults(
239+
"SELECT * FROM (SELECT *, row_number() over "
240+
"(partition by c0 order by c1) as rn FROM tmp) "
241+
"WHERE rn <= 10");
242+
243+
auto taskStats = exec::toPlanStats(task->taskStats());
244+
const auto& stats = taskStats.at(topNRowNumberId);
245+
ASSERT_EQ(stats.spilledBytes, 0);
246+
ASSERT_EQ(stats.spilledRows, 0);
247+
ASSERT_EQ(stats.spilledFiles, 0);
248+
ASSERT_EQ(stats.spilledPartitions, 0);
249+
}
250+
213251
TEST_F(TopNRowNumberTest, manyPartitions) {
214252
const vector_size_t size = 10'000;
215253
auto data = split(

0 commit comments

Comments
 (0)