Skip to content

Commit

Permalink
add bufferPool for nimble parallel writer (#103)
Browse files Browse the repository at this point in the history
Summary:

change the buffer class to a bufferPool to handle multihreaded buffers without mutexes

Differential Revision: D64774959
  • Loading branch information
Scott Young authored and facebook-github-bot committed Nov 23, 2024
1 parent 015d318 commit b9b63a3
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 38 deletions.
78 changes: 70 additions & 8 deletions dwio/nimble/common/Buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
#pragma once

#include "dwio/nimble/common/Exceptions.h"
#include "folly/fibers/Semaphore.h"
#include "velox/buffer/Buffer.h"
#include "velox/common/memory/Memory.h"

Expand All @@ -33,12 +35,11 @@
// and so on

namespace facebook::nimble {
using MemoryPool = facebook::velox::memory::MemoryPool;

// Internally manages memory in chunks. Releases memory only upon destruction.
// Internally manages memory in chunks. releases memory when destroyed
// Buffer is NOT threadsafe: external locking is required.
class Buffer {
using MemoryPool = facebook::velox::memory::MemoryPool;

public:
explicit Buffer(
MemoryPool& memoryPool,
Expand All @@ -52,7 +53,6 @@ class Buffer {
// to, and guarantees for the lifetime of *this that that region will remain
// valid. Does NOT guarantee that the region is initially 0'd.
char* reserve(uint64_t bytes) {
std::scoped_lock<std::mutex> l(mutex_);
if (reserveEnd_ + bytes <= chunkEnd_) {
pos_ = reserveEnd_;
reserveEnd_ += bytes;
Expand Down Expand Up @@ -98,11 +98,73 @@ class Buffer {
char* reserveEnd_;
std::vector<velox::BufferPtr> chunks_;
MemoryPool& memoryPool_;
// NOTE: this is temporary fix, to quickly enable parallel access to the
// buffer class. In the near future, we are going to templetize this class to
// produce a concurrent and a non-concurrent variants, and change the call
// sites to use each variant when needed.
};

// Manages a pool of buffers. Buffers are returned to the pool when released.
// maxPoolSize should be set to at least 90% of capacity for performance
class BufferPool {
public:
explicit BufferPool(
MemoryPool& memoryPool,
size_t maxPoolSize = std::thread::hardware_concurrency(),
size_t initialBufferCount = 20, // should be enough for 1 stripe
uint64_t initialChunkSize = kMinChunkSize)
: defaultInitialChunkSize_{initialChunkSize},
semaphore_{maxPoolSize},
memoryPool_{memoryPool} {
NIMBLE_CHECK(maxPoolSize > 0, "max pool size must be > 0")
NIMBLE_CHECK(
initialBufferCount <= maxPoolSize,
"initial pool size must be <= max pool size");

pool_.reserve(maxPoolSize);

for (size_t i = 0; i < initialBufferCount; ++i) {
addBuffer(newBuffer());
}
}

MemoryPool& getMemoryPool() {
return memoryPool_;
}

// buffer back to the pool.
void addBuffer(std::unique_ptr<Buffer> buffer) {
std::scoped_lock<std::mutex> lock(mutex_);
pool_.push_back(std::move(buffer));
semaphore_.signal();
}

// Reserves a buffer from the pool. Adds a new buffer to the pool
// while there are buffers available
std::unique_ptr<Buffer> reserveBuffer() {
std::scoped_lock<std::mutex> lock(mutex_);
if (pool_.empty()) {
return newBuffer();
}

semaphore_.wait();
auto buffer = std::move(pool_.back());
pool_.pop_back();
return buffer;
}

// Returns estimated number of buffers in the pool
size_t size() {
return pool_.size();
}

private:
static const uint64_t kMinChunkSize = 1LL << 20;
const uint64_t defaultInitialChunkSize_ = kMinChunkSize;
std::mutex mutex_;
folly::fibers::Semaphore semaphore_;
std::vector<std::unique_ptr<Buffer>> pool_;
MemoryPool& memoryPool_;

std::unique_ptr<Buffer> newBuffer() {
return std::make_unique<Buffer>(memoryPool_, defaultInitialChunkSize_);
}
};

} // namespace facebook::nimble
147 changes: 147 additions & 0 deletions dwio/nimble/common/tests/BufferPoolTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include <gtest/gtest.h>
#include "dwio/nimble/common/Buffer.h"
#include "dwio/nimble/common/Exceptions.h"
#include "folly/executors/CPUThreadPoolExecutor.h"
#include "velox/common/memory/Memory.h"
#include "velox/dwio/common/ExecutorBarrier.h"

namespace facebook::nimble::test {
using MemoryPool = velox::memory::MemoryPool;
using ExecutorBarrier = velox::dwio::common::ExecutorBarrier;

class BufferPoolTest : public ::testing::Test {
protected:
static void SetUpTestCase() {}

void SetUp() override {
memPool_ = velox::memory::deprecatedAddDefaultLeafMemoryPool();
}

std::shared_ptr<velox::memory::MemoryPool> memPool_;
};

TEST_F(BufferPoolTest, CreateBufferPoolWithInitialSize) {
auto bufferPool = BufferPool{*memPool_, 10, 10};
EXPECT_EQ(bufferPool.size(), 10);
}

TEST_F(BufferPoolTest, CreateBufferPoolBadMaxPool) {
try {
auto bufferPool = BufferPool{*memPool_, 0};
FAIL();
} catch (const NimbleUserError& e) {
EXPECT_EQ(e.errorMessage(), "max pool size must be > 0");
}
}

TEST_F(BufferPoolTest, CreateBufferPoolBadInitialSizeMaxSize) {
try {
auto bufferPool = BufferPool{*memPool_, 1, 2};
FAIL();
} catch (const NimbleUserError& e) {
EXPECT_EQ(e.errorMessage(), "initial pool size must be <= max pool size");
}
}

TEST_F(BufferPoolTest, ReserveBuffer) {
auto bufferPool = BufferPool{*memPool_};
auto buffer = bufferPool.reserveBuffer();
}

TEST_F(BufferPoolTest, AddBuffer) {
auto bufferPool = BufferPool{*memPool_, 10, 0};
auto buffer = bufferPool.reserveBuffer();
EXPECT_EQ(bufferPool.size(), 0);
bufferPool.addBuffer(std::move(buffer));
EXPECT_EQ(bufferPool.size(), 1);
}

TEST_F(BufferPoolTest, DestroyBufferPool) {
auto bufferPoolPtr = std::make_unique<BufferPool>(*memPool_);
bufferPoolPtr.reset();
EXPECT_NO_THROW();
}

TEST_F(BufferPoolTest, EmptyBufferPool) {
auto bufferPool = BufferPool{*memPool_, 10, 1};
EXPECT_EQ(bufferPool.size(), 1);

auto buffer1 = bufferPool.reserveBuffer();
EXPECT_EQ(bufferPool.size(), 0);
}

TEST_F(BufferPoolTest, OverfillBufferPool) {
auto bufferPool = BufferPool{*memPool_, 10, 10};
auto throwLambda = [&]() {
for (auto i = 0; i < 10; i++) {
auto buffer = std::make_unique<Buffer>(*memPool_);
bufferPool.addBuffer(std::move(buffer));
}
};

EXPECT_NO_THROW(throwLambda());
EXPECT_GE(bufferPool.size(), 1);
}

TEST_F(BufferPoolTest, EmptyFillEmptyBufferPool) {
size_t iterations = 10;
std::vector<std::unique_ptr<Buffer>> buffers;

auto bufferPool = BufferPool{*memPool_, iterations, 10};
for (auto i = 0; i < iterations; i++) {
auto buffer = bufferPool.reserveBuffer();
buffers.push_back(std::move(buffer));
}
EXPECT_EQ(bufferPool.size(), 0);
EXPECT_EQ(buffers.size(), iterations);
for (auto& buffer : buffers) {
bufferPool.addBuffer(std::move(buffer));
}
EXPECT_EQ(bufferPool.size(), iterations);
buffers.clear();

for (auto i = 0; i < iterations; i++) {
auto buffer = bufferPool.reserveBuffer();
buffers.push_back(std::move(buffer));
}
EXPECT_EQ(bufferPool.size(), 0);
EXPECT_EQ(buffers.size(), iterations);
}

TEST_F(BufferPoolTest, ParallelFillPool) {
folly::CPUThreadPoolExecutor executor{10};
ExecutorBarrier barrier{executor};
auto bufferPool = BufferPool{*memPool_, 100};
auto fillPool = [&]() {
for (auto i = 0; i < 10; i++) {
EXPECT_NO_THROW(
auto buffer = bufferPool.reserveBuffer();
std::this_thread::sleep_for(std::chrono::milliseconds{1000});
bufferPool.addBuffer(std::move(buffer)););
}
};

for (auto i = 0; i < 10; i++) {
barrier.add(fillPool);
}

barrier.waitAll();
}

} // namespace facebook::nimble::test
7 changes: 5 additions & 2 deletions dwio/nimble/velox/FieldWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,10 @@ class SimpleFieldWriter : public FieldWriter {
void write(const velox::VectorPtr& vector, const OrderedRanges& ranges)
override {
auto size = ranges.size();
auto& buffer = context_.stringBuffer();
auto& data = valuesStream_.mutableData();

auto bufferPtr = context_.bufferPool().reserveBuffer();
auto bufferGuard = folly::makeGuard(
[&]() { context_.bufferPool().addBuffer(std::move(bufferPtr)); });
if (auto flat = vector->asFlatVector<SourceType>()) {
valuesStream_.ensureNullsCapacity(flat->mayHaveNulls(), size);
bool rangeCopied = false;
Expand Down Expand Up @@ -332,6 +333,7 @@ class SimpleFieldWriter : public FieldWriter {
valuesStream_.mutableNonNulls(),
Flat<SourceType>{vector},
[&](SourceType value) {
auto& buffer = *bufferPtr;
data.push_back(
C::convert(value, buffer, valuesStream_.extraMemory()));
});
Expand All @@ -349,6 +351,7 @@ class SimpleFieldWriter : public FieldWriter {
valuesStream_.mutableNonNulls(),
Decoded<SourceType>{decoded},
[&](SourceType value) {
auto& buffer = *bufferPtr;
data.push_back(
C::convert(value, buffer, valuesStream_.extraMemory()));
});
Expand Down
23 changes: 11 additions & 12 deletions dwio/nimble/velox/FieldWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,22 @@ struct FieldWriterContext {
velox::memory::MemoryPool& memoryPool,
std::shared_ptr<folly::Executor> writeExecutor = nullptr,
std::unique_ptr<velox::memory::MemoryReclaimer> reclaimer = nullptr,
std::function<void(void)> vectorDecoderVisitor = []() {})
std::function<void(void)> vectorDecoderVisitor = []() {},
size_t maxPoolSize = std::thread::hardware_concurrency(),
size_t initialBufferCount = 20)
: bufferMemoryPool{memoryPool.addLeafChild(
"field_writer_buffer",
true,
std::move(reclaimer))},
writeExecutor{std::move(writeExecutor)},
inputBufferGrowthPolicy{
DefaultInputBufferGrowthPolicy::withDefaultRanges()},
bufferPool_{std::make_unique<BufferPool>(
*bufferMemoryPool,
maxPoolSize,
initialBufferCount)},
decodingContextPool_{std::make_unique<DecodingContextPool>(
std::move(vectorDecoderVisitor))} {
resetStringBuffer();
}
std::move(vectorDecoderVisitor))} {}

std::shared_ptr<velox::memory::MemoryPool> bufferMemoryPool;
std::shared_ptr<folly::Executor> writeExecutor;
Expand All @@ -133,13 +137,8 @@ struct FieldWriterContext {
return decodingContextPool_->addContext(std::move(context));
}

Buffer& stringBuffer() {
return *buffer_;
}

// Reset writer context for use by next stripe.
void resetStringBuffer() {
buffer_ = std::make_unique<Buffer>(*bufferMemoryPool);
BufferPool& bufferPool() {
return *bufferPool_;
}

const std::vector<std::unique_ptr<StreamData>>& streams() {
Expand Down Expand Up @@ -169,7 +168,7 @@ struct FieldWriterContext {
}

private:
std::unique_ptr<Buffer> buffer_;
std::unique_ptr<BufferPool> bufferPool_;
std::unique_ptr<DecodingContextPool> decodingContextPool_;
std::vector<std::unique_ptr<StreamData>> streams_;
};
Expand Down
19 changes: 9 additions & 10 deletions dwio/nimble/velox/VeloxWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ class WriterContext : public FieldWriterContext {
memoryPool,
options.writeExecutor,
options.reclaimerFactory(),
options.vectorDecoderVisitor
options.vectorDecoderVisitor,
options.maxPoolSize,
options.initialBufferCount
},
options{std::move(options)},
logger{this->options.metricsLogger} {
Expand Down Expand Up @@ -620,9 +622,6 @@ void VeloxWriter::writeChunk(bool lastChunk) {
LoggingScope scope{*context_->logger};
velox::CpuWallTimer veloxTimer{context_->stripeFlushTiming};

if (!encodingBuffer_) {
encodingBuffer_ = std::make_unique<Buffer>(*encodingMemoryPool_);
}
streams_.resize(context_->schemaBuilder.nodeCount());

// When writing null streams, we write the nulls as data, and the stream
Expand Down Expand Up @@ -666,9 +665,13 @@ void VeloxWriter::writeChunk(bool lastChunk) {

auto encode = [&](StreamData& streamData) {
const auto offset = streamData.descriptor().offset();
auto encoded = encodeStream(*context_, *encodingBuffer_, streamData);
auto bufferPtr = context_->bufferPool().reserveBuffer();
auto bufferGuard = folly::makeGuard(
[&]() { context_->bufferPool().addBuffer(std::move(bufferPtr)); });
auto& buffer = *bufferPtr;
auto encoded = encodeStream(*context_, buffer, streamData);
if (!encoded.empty()) {
ChunkedStreamWriter chunkWriter{*encodingBuffer_};
ChunkedStreamWriter chunkWriter{buffer};
NIMBLE_DASSERT(offset < streams_.size(), "Stream offset out of range.");
auto& stream = streams_[offset];
for (auto& buffer : chunkWriter.encode(encoded)) {
Expand Down Expand Up @@ -780,10 +783,6 @@ uint32_t VeloxWriter::writeStripe() {
uint64_t startSize = writer_.size();
writer_.writeStripe(context_->rowsInStripe, std::move(streams_));
stripeSize = writer_.size() - startSize;
encodingBuffer_.reset();
// TODO: once chunked string fields are supported, move string buffer
// reset to writeChunk()
context_->resetStringBuffer();
}

NIMBLE_ASSERT(
Expand Down
1 change: 0 additions & 1 deletion dwio/nimble/velox/VeloxWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ class VeloxWriter {
TabletWriter writer_;
std::unique_ptr<FieldWriter> root_;

std::unique_ptr<Buffer> encodingBuffer_;
std::vector<Stream> streams_;
std::exception_ptr lastException_;
const velox::common::SpillConfig* const spillConfig_;
Expand Down
Loading

0 comments on commit b9b63a3

Please sign in to comment.